I18n

EBO <eric.bouhana@softalys.com> committé le 2026-03-09 09:43

955fb60464d45f79303f830a2b64dacc8fccd797

1 parent(s)

19 fichiers modifiés +2454 -883
M locales/de.yml
+227 -1
@@ -34,6 +34,7 @@ login:
34 34 resend_prompt: "Bitte geben Sie Ihre E-Mail ein, bevor Sie erneut senden."
35 35 register_success: "Konto erfolgreich erstellt! Bitte überprüfen Sie Ihre E-Mail."
36 36 register_error: "Registrierungsfehler"
37 + email_label: "E-Mail"
37 38 footer_text: "Nextcloud Fork in Rust"
38 39
39 40 # Fehlermeldungen
@@ -63,14 +64,86 @@ error:
63 64 password_require_digits: "Das Passwort muss mindestens eine Ziffer enthalten"
64 65 password_require_special: "Das Passwort muss mindestens ein Sonderzeichen (!@#$%^&*()_+-=[]{}|;:,.<>?) enthalten"
65 66
67 +# Bestätigungs-E-Mail erneut senden
68 +resend_verification:
69 + page_title: "Bestätigungs-E-Mail erneut senden - RustWarden"
70 + title: "Bestätigungs-E-Mail erneut senden"
71 + subtitle: "Geben Sie Ihre E-Mail-Adresse ein, um einen neuen Bestätigungslink zu erhalten."
72 + email_label: "E-Mail-Adresse"
73 + email_placeholder: "sie@beispiel.de"
74 + submit: "Bestätigungslink senden"
75 + back_to_login: "Zurück zur Anmeldung"
76 + error_default: "Die Anfrage konnte nicht gesendet werden."
77 + success_default: "Wenn das Konto existiert und nicht verifiziert ist, wurde eine E-Mail gesendet."
78 + error_missing_email: "Bitte geben Sie Ihre E-Mail ein."
79 + error_cannot_send: "Die Anfrage kann derzeit nicht gesendet werden."
80 +
81 +# Passwort zurücksetzen (über E-Mail-Link)
82 +password_reset:
83 + page_title: "Passwort zurücksetzen"
84 + title: "Passwort zurücksetzen"
85 + subtitle: "Geben Sie unten Ihr neues Passwort ein."
86 + new_password_label: "Neues Passwort"
87 + new_password_placeholder: "Neues sicheres Passwort"
88 + requirements: "⚠️ Muss enthalten: 8+ Zeichen, Großbuchstabe, Kleinbuchstabe, Ziffer, Sonderzeichen"
89 + confirm_password_label: "Passwort bestätigen"
90 + confirm_password_placeholder: "Bestätigen Sie Ihr neues Passwort"
91 + missing_token_url: "Zurücksetzungs-Token fehlt in der URL."
92 + missing_token: "Zurücksetzungs-Token fehlt."
93 + missing_new_password: "Bitte geben Sie ein neues Passwort ein."
94 + passwords_not_match: "Die Passwörter stimmen nicht überein."
95 + success_message: "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt anmelden."
96 + error_link_invalid: "Das Passwort konnte nicht zurückgesetzt werden. Der Link ist möglicherweise abgelaufen oder ungültig."
97 + error_generic: "Das Passwort kann derzeit nicht zurückgesetzt werden."
98 +
66 99 # Einstellungen
67 100 settings:
68 101 title: "Einstellungen"
102 + page_title: "Einstellungen - RustWarden"
103 + subtitle: "Verwalten Sie Ihre Präferenzen und Kontoeinstellungen"
104 + profile_title: "Mein Profil"
105 + profile_desc: "Bearbeiten Sie Ihre persönlichen Informationen (Benutzername, E-Mail)"
106 + password_title: "Passwort"
107 + password_desc: "Ändern Sie Ihr Passwort, um Ihr Konto zu sichern"
108 + sessions_title: "Aktive Sitzungen"
109 + sessions_desc: "Verwalten Sie Ihre aktiven Sitzungen und widerrufen Sie den Zugriff von anderen Geräten"
110 + preferences_title: "Präferenzen"
111 + preferences_desc: "Passen Sie Ihre Erfahrung an (Design, Sprache)"
112 + access: "Zugriff"
113 + info_title: "Informationen"
114 + info_desc: "Verwenden Sie die obigen Abschnitte, um verschiedene Aspekte Ihres Kontos zu verwalten. Alle Änderungen sind sicher und erfordern Ihre Authentifizierung."
69 115 language: "Sprache"
70 116 language_description: "Wählen Sie Ihre bevorzugte Sprache"
71 117 save: "Speichern"
72 118 saved: "Einstellungen gespeichert"
73 119
120 +# Startseite
121 +home:
122 + welcome: "Willkommen, {username}!"
123 + subtitle: "Willkommen in Ihrem persönlichen Bereich"
124 + my_account: "Mein Konto"
125 + upcoming_features: "Kommende Funktionen"
126 + stat_username: "Benutzername"
127 + stat_email: "E-Mail"
128 + stat_member_since: "Mitglied seit"
129 + stat_roles: "Rollen"
130 + feature_secrets_name: "Geheimnisse-Manager"
131 + feature_secrets_desc: "Sichere Speicherung von Passwörtern und Geheimnissen"
132 + feature_2fa_name: "2FA-Authentifizierung"
133 + feature_2fa_desc: "Zwei-Faktor-Authentifizierung"
134 + feature_files_name: "Dateimanager"
135 + feature_files_desc: "Dateien und Ordner durchsuchen, erstellen, umbenennen und löschen"
136 + feature_upload_name: "Upload/Download"
137 + feature_upload_desc: "Dateien mit Fortschrittsbalken hoch- und herunterladen"
138 + feature_webdav_name: "WebDAV & Synchronisierung"
139 + feature_webdav_desc: "Dateien mit Desktop-Clients über WebDAV synchronisieren"
140 + feature_mobile_name: "Mobile API"
141 + feature_mobile_desc: "Synchronisierung mit mobilen Apps (iOS/Android)"
142 + badge_soon: "Bald"
143 + badge_planned: "Geplant"
144 + did_you_know: "Wussten Sie schon?"
145 + did_you_know_text: "RustWarden ist ein sicherer Passwort-Manager, der in Rust geschrieben wurde, um bessere Leistung und erhöhte Sicherheit zu bieten. Bleiben Sie dran für neue Funktionen!"
146 +
74 147 # Allgemeine Nachrichten
75 148 common:
76 149 welcome: "Willkommen {username}"
@@ -154,8 +227,120 @@ breadcrumb:
154 227 password: "Passwort"
155 228 sessions: "Sitzungen"
156 229
157 -# Admin-Einstellungen
230 +# Admin
158 231 admin:
232 + dashboard:
233 + title: "Verwaltung"
234 + heading: "Administrator-Dashboard"
235 + users_card_title: "Benutzer"
236 + users_card_subtitle: "Benutzer gesamt"
237 + access_button: "Zugreifen"
238 + sessions_card_title: "Aktive Sitzungen"
239 + sessions_card_subtitle: "Laufende Sitzungen"
240 + admins_card_title: "Administratoren"
241 + admins_card_subtitle: "Admin-Konten"
242 + last_activity_card_title: "Letzte Aktivität"
243 + last_activity_card_subtitle: "Letzte Anmeldung"
244 + audit_section_title: "Audit-Log-Statistiken"
245 + audit_total_logs: "Logs gesamt"
246 + audit_last_24h: "Letzte 24h"
247 + audit_by_level: "Nach Stufe"
248 + chart_label: "Logs nach Stufe"
249 + settings_card_title: "Anwendungseinstellungen"
250 + settings_card_desc: "Globale Anwendungseinstellungen konfigurieren"
251 + apps_card_title: "Anwendungsverwaltung"
252 + apps_card_desc: "Modulare Anwendungen aktivieren oder deaktivieren"
253 + orgs_card_title: "Organisationsverwaltung"
254 + orgs_card_desc: "Organisationen und deren Mitglieder erstellen und verwalten"
255 + orgs_stat_title: "Organisationen"
256 + orgs_members_stat_title: "Mitglieder gesamt"
257 + errors:
258 + title: "Fehlercodes"
259 + description: "Dokumentation der Anwendungsfehlercodes. Jeder Fehlercode ist mit einer Middleware verknüpft und liefert Kontextinformationen für die Fehlersuche."
260 + breadcrumb_errors: "Fehlercodes"
261 + table_code: "Code"
262 + table_description: "Beschreibung"
263 + table_category: "Kategorie"
264 + table_middleware: "Middleware"
265 + table_context: "Kontext"
266 + table_example: "Beispiel"
267 + legend_title: "Legende"
268 + legend_code: "Eindeutige Fehler-ID (Format: CATEGORY_NUMBER)"
269 + legend_description: "Beschreibung des Fehlers"
270 + legend_category: "Fehlertyp (Authentifizierung, CSRF, Rate Limiting, etc.)"
271 + legend_middleware: "Middleware, die diesen Fehler erzeugt"
272 + legend_context: "Detaillierte Erklärung, wann dieser Fehler auftritt"
273 + legend_example: "Konkretes Beispiel einer Situation, die diesen Fehler erzeugt"
274 + error_auth001_desc: "Ungültige Anmeldedaten"
275 + error_auth001_category: "Authentifizierung"
276 + error_auth001_context: "Der Benutzer hat bei der Anmeldung falsche Anmeldedaten eingegeben"
277 + error_auth001_example: "Anmeldeversuch mit falschem Passwort"
278 + error_auth002_desc: "Ungültiges Token"
279 + error_auth002_category: "Authentifizierung"
280 + error_auth002_context: "Das bereitgestellte JWT-Token ist ungültig oder fehlerhaft"
281 + error_auth002_example: "Beschädigtes JWT-Token oder mit anderem Schlüssel signiert"
282 + error_auth003_desc: "Token abgelaufen"
283 + error_auth003_category: "Authentifizierung"
284 + error_auth003_context: "Das JWT-Token ist abgelaufen und muss erneuert werden"
285 + error_auth003_example: "Zugriffsversuch mit abgelaufenem Token nach der Gültigkeitsdauer"
286 + error_csrf001_desc: "CSRF-Token fehlt"
287 + error_csrf001_category: "CSRF-Schutz"
288 + error_csrf001_context: "Die POST/PUT/DELETE-Anfrage enthält kein CSRF-Token"
289 + error_csrf001_example: "Formular ohne CSRF-Token im Header oder Body abgesendet"
290 + error_csrf002_desc: "Ungültiges CSRF-Token"
291 + error_csrf002_category: "CSRF-Schutz"
292 + error_csrf002_context: "Das bereitgestellte CSRF-Token stimmt nicht mit dem Sitzungs-Token überein"
293 + error_csrf002_example: "Geändertes oder gestohlenes CSRF-Token"
294 + error_rate001_desc: "Ratenlimit überschritten"
295 + error_rate001_category: "Rate Limiting"
296 + error_rate001_context: "Die Anzahl der Anfragen pro Minute wurde überschritten"
297 + error_rate001_example: "Mehr als 100 Anfragen/Min auf Admin-Endpunkten"
298 + error_valid001_desc: "Ungültige Eingabedaten"
299 + error_valid001_category: "Validierung"
300 + error_valid001_context: "Die bereitgestellten Daten entsprechen nicht den Validierungsregeln"
301 + error_valid001_example: "Ungültiges E-Mail-Format oder fehlendes Pflichtfeld"
302 + error_valid002_desc: "Passwortkomplexität nicht erfüllt"
303 + error_valid002_category: "Validierung"
304 + error_valid002_context: "Das Passwort erfüllt nicht die konfigurierten Komplexitätsregeln"
305 + error_valid002_example: "Passwort zu kurz oder fehlende erforderliche Zeichen"
306 + error_rbac001_desc: "Unzureichende Berechtigungen"
307 + error_rbac001_category: "Autorisierung"
308 + error_rbac001_context: "Der Benutzer hat nicht die erforderlichen Berechtigungen für diese Aktion"
309 + error_rbac001_example: "Nicht-Admin-Benutzer versucht auf /admin/users zuzugreifen"
310 + error_rbac002_desc: "Administratorrolle erforderlich"
311 + error_rbac002_category: "Autorisierung"
312 + error_rbac002_context: "Die Aktion erfordert eine Administratorrolle"
313 + error_rbac002_example: "Versuch einen Benutzer ohne Admin-Rolle zu löschen"
314 + error_db001_desc: "Datenbankoperationsfehler"
315 + error_db001_category: "Datenbank"
316 + error_db001_context: "Eine Datenbankoperation ist fehlgeschlagen"
317 + error_db001_example: "Fremdschlüssel-Constraint verletzt oder DB-Verbindung verloren"
318 + error_db002_desc: "Ressource nicht gefunden"
319 + error_db002_category: "Datenbank"
320 + error_db002_context: "Die angeforderte Ressource existiert nicht in der Datenbank"
321 + error_db002_example: "Benutzer mit nicht existierender ID"
322 + user_form:
323 + breadcrumb_create: "Erstellen"
324 + breadcrumb_edit: "Bearbeiten"
325 + title_create: "Benutzer erstellen"
326 + title_edit: "Benutzer bearbeiten"
327 + back_to_list: "Zurück zur Liste"
328 + username_label: "Benutzername"
329 + username_placeholder: "benutzername"
330 + username_readonly_hint: "Der Benutzername kann nach der Erstellung nicht geändert werden"
331 + email_label: "E-Mail"
332 + email_placeholder: "benutzer@example.com"
333 + password_label: "Passwort"
334 + password_optional: "Optional"
335 + password_placeholder: "Leer lassen, um ein zufälliges Passwort zu generieren"
336 + password_hint: "Wenn leer gelassen, wird automatisch ein sicheres Passwort generiert"
337 + roles_label: "Rollen"
338 + roles_hint: "Wählen Sie mindestens eine Rolle"
339 + must_change_password_label: "Passwortänderung erzwingen"
340 + must_change_password_desc: "Der Benutzer muss sein Passwort bei der nächsten Anmeldung ändern"
341 + cancel: "Abbrechen"
342 + submit_create: "Benutzer erstellen"
343 + submit_edit: "Änderungen speichern"
159 344 settings:
160 345 title: "Anwendungseinstellungen"
161 346 page_title: "Anwendungseinstellungen - Admin - RustCloud"
@@ -174,6 +359,34 @@ admin:
174 359 saved: "Einstellungen erfolgreich gespeichert"
175 360 error: "Fehler beim Speichern der Einstellungen"
176 361 network_error: "Netzwerkfehler. Bitte versuchen Sie es erneut."
362 + password_security: "Passwortsicherheit"
363 + password_min_length: "Mindestlänge"
364 + password_min_length_hint: "Zwischen 6 und 128 Zeichen"
365 + password_complexity: "Komplexitätsanforderungen"
366 + require_uppercase: "Großbuchstabe erforderlich"
367 + require_lowercase: "Kleinbuchstabe erforderlich"
368 + require_digits: "Ziffer erforderlich"
369 + require_special: "Sonderzeichen erforderlich"
370 + password_expiration: "Passwortablauf"
371 + password_expiration_desc: "Periodische Passwortänderung erzwingen"
372 + expiration_days: "Ablauf nach (Tagen)"
373 + expiration_alert: "E-Mail-Warnung vor Ablauf"
374 + expiration_alert_days: "Warnung (Tage vorher)"
375 + audit_logging: "Audit-Protokollierung"
376 + log_level: "Protokollebene"
377 + log_level_debug: "DEBUG - Alle Details"
378 + log_level_info: "INFO - Normale Informationen"
379 + log_level_warn: "WARN - Nur Warnungen"
380 + log_level_error: "ERROR - Nur Fehler"
381 + log_level_hint: "Steuert die Ausführlichkeit der Audit-Logs"
382 + log_retention: "Log-Aufbewahrung (Tage)"
383 + log_retention_hint: "Aufbewahrungsdauer der Logs (1-365 Tage)"
384 + audit_info_message: "Audit-Logs zeichnen alle administrativen Aktionen auf (Erstellung, Änderung, Löschung von Benutzern)."
385 + save_changes: "Änderungen speichern"
386 + save_changes_desc: "Einstellungen werden sofort angewendet"
387 + saving: "Speichern..."
388 + save_success: "Einstellungen erfolgreich gespeichert!"
389 + save_error_prefix: "Fehler"
177 390 users_list:
178 391 page_title: "Benutzerverwaltung - Admin - RustCloud"
179 392 title: "Benutzerverwaltung"
@@ -211,8 +424,21 @@ admin:
211 424 delete_confirm_email: "E-Mail:"
212 425 delete_irreversible: "Diese Aktion ist unwiderruflich!"
213 426 delete_button: "Endgültig löschen"
427 + delete: "Löschen"
214 428 delete_error: "Fehler beim Löschen des Benutzers"
215 429 network_error: "Netzwerkfehler"
430 + edit_user_title: "Benutzer bearbeiten"
431 + resetting: "Zurücksetzen..."
432 + reset_by_email_title: "Zurücksetzen per E-Mail"
433 + reset_email: "Per E-Mail zurücksetzen"
434 + close: "Schließen"
435 + password_reset_success_title: "Passwort zurückgesetzt"
436 + password_warning_once: "Dieses Passwort wird nur einmal angezeigt. Kopieren Sie es jetzt!"
437 + new_password_label: "Neues Passwort"
438 + copy: "Kopieren"
439 + password_copied: "Passwort in die Zwischenablage kopiert"
440 + password_set_success: "Passwort erfolgreich zurückgesetzt"
441 + reset_error: "Fehler beim Zurücksetzen des Passworts"
216 442
217 443 # Passwort ändern
218 444 password:
M locales/en.yml
+226 -1
@@ -34,8 +34,41 @@ login:
34 34 resend_prompt: "Please enter your email before resending."
35 35 register_success: "Account created successfully! Please check your email."
36 36 register_error: "Registration error"
37 + email_label: "Email"
37 38 footer_text: "Nextcloud fork in Rust"
38 39
40 +# Resend verification email
41 +resend_verification:
42 + page_title: "Resend verification email - RustWarden"
43 + title: "Resend verification email"
44 + subtitle: "Enter your email address to receive a new verification link."
45 + email_label: "Email address"
46 + email_placeholder: "you@example.com"
47 + submit: "Send verification link"
48 + back_to_login: "Back to login"
49 + error_default: "Unable to send the request."
50 + success_default: "If the account exists and is not verified, an email has been sent."
51 + error_missing_email: "Please enter your email."
52 + error_cannot_send: "Unable to send the request at this time."
53 +
54 +# Password reset (from email link)
55 +password_reset:
56 + page_title: "Reset password"
57 + title: "Reset password"
58 + subtitle: "Enter your new password below."
59 + new_password_label: "New password"
60 + new_password_placeholder: "New secure password"
61 + requirements: "⚠️ Must contain: 8+ characters, uppercase, lowercase, digit, special character"
62 + confirm_password_label: "Confirm password"
63 + confirm_password_placeholder: "Confirm your new password"
64 + missing_token_url: "Reset token missing in URL."
65 + missing_token: "Reset token is missing."
66 + missing_new_password: "Please enter a new password."
67 + passwords_not_match: "Passwords do not match."
68 + success_message: "Your password has been reset successfully. You can now log in."
69 + error_link_invalid: "Unable to reset password. The link may be expired or invalid."
70 + error_generic: "Unable to reset password at this time."
71 +
39 72 # Error messages
40 73 error:
41 74 invalid_credentials: "Invalid credentials"
@@ -66,11 +99,51 @@ error:
66 99 # Settings
67 100 settings:
68 101 title: "Settings"
102 + page_title: "Settings - RustWarden"
103 + subtitle: "Manage your preferences and account settings"
104 + profile_title: "My Profile"
105 + profile_desc: "Edit your personal information (username, email)"
106 + password_title: "Password"
107 + password_desc: "Change your password to secure your account"
108 + sessions_title: "Active Sessions"
109 + sessions_desc: "Manage your active sessions and revoke access from other devices"
110 + preferences_title: "Preferences"
111 + preferences_desc: "Customize your experience (theme, language)"
112 + access: "Access"
113 + info_title: "Information"
114 + info_desc: "Use the sections above to manage different aspects of your account. All changes are secure and require your authentication."
69 115 language: "Language"
70 116 language_description: "Choose your preferred language"
71 117 save: "Save"
72 118 saved: "Settings saved"
73 119
120 +# Home page
121 +home:
122 + welcome: "Welcome, {username}!"
123 + subtitle: "Welcome to your personal space"
124 + my_account: "My Account"
125 + upcoming_features: "Upcoming Features"
126 + stat_username: "Username"
127 + stat_email: "Email"
128 + stat_member_since: "Member since"
129 + stat_roles: "Roles"
130 + feature_secrets_name: "Secrets Manager"
131 + feature_secrets_desc: "Secure storage for passwords and secrets"
132 + feature_2fa_name: "2FA Authentication"
133 + feature_2fa_desc: "Two-factor authentication"
134 + feature_files_name: "File Manager"
135 + feature_files_desc: "Browse, create, rename and delete your files and folders"
136 + feature_upload_name: "Upload/Download"
137 + feature_upload_desc: "Download and upload files with progress bar"
138 + feature_webdav_name: "WebDAV & Sync"
139 + feature_webdav_desc: "Sync your files with desktop clients via WebDAV"
140 + feature_mobile_name: "Mobile API"
141 + feature_mobile_desc: "Sync with mobile apps (iOS/Android)"
142 + badge_soon: "Soon"
143 + badge_planned: "Planned"
144 + did_you_know: "Did You Know?"
145 + did_you_know_text: "RustWarden is a secure password manager written in Rust for better performance and enhanced security. Stay tuned for new features!"
146 +
74 147 # Common messages
75 148 common:
76 149 welcome: "Welcome {username}"
@@ -159,8 +232,120 @@ breadcrumb:
159 232 password: "Password"
160 233 sessions: "Sessions"
161 234
162 -# Admin Settings
235 +# Admin
163 236 admin:
237 + dashboard:
238 + title: "Administration"
239 + heading: "Administrator Dashboard"
240 + users_card_title: "Users"
241 + users_card_subtitle: "Total users"
242 + access_button: "Access"
243 + sessions_card_title: "Active Sessions"
244 + sessions_card_subtitle: "Ongoing sessions"
245 + admins_card_title: "Administrators"
246 + admins_card_subtitle: "Admin accounts"
247 + last_activity_card_title: "Last Activity"
248 + last_activity_card_subtitle: "Last login"
249 + audit_section_title: "Audit Log Statistics"
250 + audit_total_logs: "Total logs"
251 + audit_last_24h: "Last 24h"
252 + audit_by_level: "By level"
253 + chart_label: "Logs by level"
254 + settings_card_title: "Application Settings"
255 + settings_card_desc: "Configure global application settings"
256 + apps_card_title: "Application Management"
257 + apps_card_desc: "Enable or disable modular applications"
258 + orgs_card_title: "Organisation Management"
259 + orgs_card_desc: "Create and manage organisations and their members"
260 + orgs_stat_title: "Organisations"
261 + orgs_members_stat_title: "Total members"
262 + errors:
263 + title: "Error Codes"
264 + description: "Application error code documentation. Each error code is associated with a middleware and provides context information for debugging."
265 + breadcrumb_errors: "Error Codes"
266 + table_code: "Code"
267 + table_description: "Description"
268 + table_category: "Category"
269 + table_middleware: "Middleware"
270 + table_context: "Context"
271 + table_example: "Example"
272 + legend_title: "Legend"
273 + legend_code: "Unique error identifier (format: CATEGORY_NUMBER)"
274 + legend_description: "Description of the error"
275 + legend_category: "Error type (Authentication, CSRF, Rate Limiting, etc.)"
276 + legend_middleware: "Middleware responsible for generating this error"
277 + legend_context: "Detailed explanation of when this error occurs"
278 + legend_example: "Concrete example of a situation generating this error"
279 + error_auth001_desc: "Invalid credentials"
280 + error_auth001_category: "Authentication"
281 + error_auth001_context: "The user provided incorrect credentials during login"
282 + error_auth001_example: "Login attempt with an incorrect password"
283 + error_auth002_desc: "Invalid token"
284 + error_auth002_category: "Authentication"
285 + error_auth002_context: "The provided JWT token is invalid or malformed"
286 + error_auth002_example: "Corrupted JWT token or signed with a different key"
287 + error_auth003_desc: "Token expired"
288 + error_auth003_category: "Authentication"
289 + error_auth003_context: "The JWT token has expired and needs to be renewed"
290 + error_auth003_example: "Access attempt with an expired token after the validity period"
291 + error_csrf001_desc: "Missing CSRF token"
292 + error_csrf001_category: "CSRF Protection"
293 + error_csrf001_context: "The POST/PUT/DELETE request does not contain a CSRF token"
294 + error_csrf001_example: "Form submitted without a CSRF token in the header or body"
295 + error_csrf002_desc: "Invalid CSRF token"
296 + error_csrf002_category: "CSRF Protection"
297 + error_csrf002_context: "The provided CSRF token does not match the session token"
298 + error_csrf002_example: "Modified or stolen CSRF token"
299 + error_rate001_desc: "Rate limit exceeded"
300 + error_rate001_category: "Rate Limiting"
301 + error_rate001_context: "The number of requests per minute has been exceeded"
302 + error_rate001_example: "More than 100 requests/min on admin endpoints"
303 + error_valid001_desc: "Invalid input data"
304 + error_valid001_category: "Validation"
305 + error_valid001_context: "The provided data does not comply with validation rules"
306 + error_valid001_example: "Invalid email format or missing required field"
307 + error_valid002_desc: "Password complexity not met"
308 + error_valid002_category: "Validation"
309 + error_valid002_context: "The password does not meet the configured complexity rules"
310 + error_valid002_example: "Password too short or missing required characters"
311 + error_rbac001_desc: "Insufficient permissions"
312 + error_rbac001_category: "Authorization"
313 + error_rbac001_context: "The user does not have the necessary permissions for this action"
314 + error_rbac001_example: "Non-admin user attempting to access /admin/users"
315 + error_rbac002_desc: "Admin role required"
316 + error_rbac002_category: "Authorization"
317 + error_rbac002_context: "The action requires an administrator role"
318 + error_rbac002_example: "Attempting to delete a user without admin role"
319 + error_db001_desc: "Database operation error"
320 + error_db001_category: "Database"
321 + error_db001_context: "A database operation failed"
322 + error_db001_example: "Foreign key constraint violated or DB connection lost"
323 + error_db002_desc: "Resource not found"
324 + error_db002_category: "Database"
325 + error_db002_context: "The requested resource does not exist in the database"
326 + error_db002_example: "User with non-existent ID"
327 + user_form:
328 + breadcrumb_create: "Create"
329 + breadcrumb_edit: "Edit"
330 + title_create: "Create a user"
331 + title_edit: "Edit a user"
332 + back_to_list: "Back to list"
333 + username_label: "Username"
334 + username_placeholder: "username"
335 + username_readonly_hint: "Username cannot be changed after creation"
336 + email_label: "Email"
337 + email_placeholder: "user@example.com"
338 + password_label: "Password"
339 + password_optional: "Optional"
340 + password_placeholder: "Leave empty to generate a random password"
341 + password_hint: "If left empty, a secure password will be automatically generated"
342 + roles_label: "Roles"
343 + roles_hint: "Select at least one role"
344 + must_change_password_label: "Force password change"
345 + must_change_password_desc: "The user will have to change their password at next login"
346 + cancel: "Cancel"
347 + submit_create: "Create user"
348 + submit_edit: "Save changes"
164 349 settings:
165 350 title: "Application Settings"
166 351 page_title: "Application Settings - Admin - RustCloud"
@@ -179,6 +364,34 @@ admin:
179 364 saved: "Settings saved successfully"
180 365 error: "Error saving settings"
181 366 network_error: "Network error. Please try again."
367 + password_security: "Password Security"
368 + password_min_length: "Minimum length"
369 + password_min_length_hint: "Between 6 and 128 characters"
370 + password_complexity: "Complexity requirements"
371 + require_uppercase: "Uppercase required"
372 + require_lowercase: "Lowercase required"
373 + require_digits: "Digit required"
374 + require_special: "Special character required"
375 + password_expiration: "Password expiration"
376 + password_expiration_desc: "Force periodic password change"
377 + expiration_days: "Expiration after (days)"
378 + expiration_alert: "Email alert before expiration"
379 + expiration_alert_days: "Alert (days before)"
380 + audit_logging: "Audit Logging"
381 + log_level: "Log level"
382 + log_level_debug: "DEBUG - All details"
383 + log_level_info: "INFO - Normal information"
384 + log_level_warn: "WARN - Warnings only"
385 + log_level_error: "ERROR - Errors only"
386 + log_level_hint: "Controls the verbosity of audit logs"
387 + log_retention: "Log retention (days)"
388 + log_retention_hint: "Log retention period (1-365 days)"
389 + audit_info_message: "Audit logs record all administrative actions (creation, modification, deletion of users)."
390 + save_changes: "Save changes"
391 + save_changes_desc: "Settings will be applied immediately"
392 + saving: "Saving..."
393 + save_success: "Settings saved successfully!"
394 + save_error_prefix: "Error"
182 395 users_list:
183 396 page_title: "User Management - Admin - RustCloud"
184 397 title: "User Management"
@@ -217,8 +430,20 @@ admin:
217 430 delete_confirm_email: "Email:"
218 431 delete_irreversible: "This action is irreversible!"
219 432 delete_button: "Delete permanently"
433 + delete: "Delete"
220 434 delete_error: "Error deleting user"
221 435 network_error: "Network error"
436 + edit_user_title: "Edit user"
437 + resetting: "Resetting..."
438 + reset_by_email_title: "Reset by email"
439 + close: "Close"
440 + password_reset_success_title: "Password reset"
441 + password_warning_once: "This password will only be displayed once. Copy it now!"
442 + new_password_label: "New password"
443 + copy: "Copy"
444 + password_copied: "Password copied to clipboard"
445 + password_set_success: "Password reset successfully"
446 + reset_error: "Error resetting password"
222 447
223 448 # Password Change
224 449 password:
M locales/es.yml
+227 -1
@@ -34,6 +34,7 @@ login:
34 34 resend_prompt: "Por favor ingrese su email antes de reenviar."
35 35 register_success: "¡Cuenta creada con éxito! Por favor verifique su email."
36 36 register_error: "Error en el registro"
37 + email_label: "Correo electrónico"
37 38 footer_text: "Fork de Nextcloud en Rust"
38 39
39 40 # Mensajes de error
@@ -63,14 +64,86 @@ error:
63 64 password_require_digits: "La contraseña debe contener al menos un dígito"
64 65 password_require_special: "La contraseña debe contener al menos un carácter especial (!@#$%^&*()_+-=[]{}|;:,.<>?)"
65 66
67 +# Reenviar email de verificación
68 +resend_verification:
69 + page_title: "Reenviar email de verificación - RustWarden"
70 + title: "Reenviar email de verificación"
71 + subtitle: "Ingrese su dirección de correo electrónico para recibir un nuevo enlace de verificación."
72 + email_label: "Dirección de correo electrónico"
73 + email_placeholder: "usted@ejemplo.com"
74 + submit: "Enviar enlace de verificación"
75 + back_to_login: "Volver al inicio de sesión"
76 + error_default: "No se pudo enviar la solicitud."
77 + success_default: "Si la cuenta existe y no está verificada, se ha enviado un email."
78 + error_missing_email: "Por favor ingrese su correo electrónico."
79 + error_cannot_send: "No se puede enviar la solicitud en este momento."
80 +
81 +# Restablecimiento de contraseña (desde enlace de email)
82 +password_reset:
83 + page_title: "Restablecer contraseña"
84 + title: "Restablecer contraseña"
85 + subtitle: "Ingrese su nueva contraseña a continuación."
86 + new_password_label: "Nueva contraseña"
87 + new_password_placeholder: "Nueva contraseña segura"
88 + requirements: "⚠️ Debe contener: 8+ caracteres, mayúscula, minúscula, dígito, carácter especial"
89 + confirm_password_label: "Confirmar contraseña"
90 + confirm_password_placeholder: "Confirme su nueva contraseña"
91 + missing_token_url: "Falta el token de restablecimiento en la URL."
92 + missing_token: "Falta el token de restablecimiento."
93 + missing_new_password: "Por favor ingrese una nueva contraseña."
94 + passwords_not_match: "Las contraseñas no coinciden."
95 + success_message: "Su contraseña ha sido restablecida con éxito. Ahora puede iniciar sesión."
96 + error_link_invalid: "No se pudo restablecer la contraseña. El enlace puede haber expirado o ser inválido."
97 + error_generic: "No se puede restablecer la contraseña en este momento."
98 +
66 99 # Configuración
67 100 settings:
68 101 title: "Configuración"
102 + page_title: "Configuración - RustWarden"
103 + subtitle: "Gestione sus preferencias y configuración de cuenta"
104 + profile_title: "Mi Perfil"
105 + profile_desc: "Edite su información personal (nombre de usuario, email)"
106 + password_title: "Contraseña"
107 + password_desc: "Cambie su contraseña para asegurar su cuenta"
108 + sessions_title: "Sesiones activas"
109 + sessions_desc: "Gestione sus sesiones activas y revoque el acceso desde otros dispositivos"
110 + preferences_title: "Preferencias"
111 + preferences_desc: "Personalice su experiencia (tema, idioma)"
112 + access: "Acceder"
113 + info_title: "Información"
114 + info_desc: "Utilice las secciones anteriores para gestionar diferentes aspectos de su cuenta. Todos los cambios son seguros y requieren su autenticación."
69 115 language: "Idioma"
70 116 language_description: "Elija su idioma preferido"
71 117 save: "Guardar"
72 118 saved: "Configuración guardada"
73 119
120 +# Página de inicio
121 +home:
122 + welcome: "¡Bienvenido, {username}!"
123 + subtitle: "Bienvenido a tu espacio personal"
124 + my_account: "Mi Cuenta"
125 + upcoming_features: "Próximas Funcionalidades"
126 + stat_username: "Nombre de usuario"
127 + stat_email: "Correo electrónico"
128 + stat_member_since: "Miembro desde"
129 + stat_roles: "Roles"
130 + feature_secrets_name: "Gestor de secretos"
131 + feature_secrets_desc: "Almacenamiento seguro de contraseñas y secretos"
132 + feature_2fa_name: "Autenticación 2FA"
133 + feature_2fa_desc: "Autenticación de dos factores"
134 + feature_files_name: "Gestor de Archivos"
135 + feature_files_desc: "Explorar, crear, renombrar y eliminar archivos y carpetas"
136 + feature_upload_name: "Subir/Descargar"
137 + feature_upload_desc: "Descargar y subir archivos con barra de progreso"
138 + feature_webdav_name: "WebDAV y Sincronización"
139 + feature_webdav_desc: "Sincronizar archivos con clientes de escritorio vía WebDAV"
140 + feature_mobile_name: "API Móvil"
141 + feature_mobile_desc: "Sincronización con aplicaciones móviles (iOS/Android)"
142 + badge_soon: "Pronto"
143 + badge_planned: "Planificado"
144 + did_you_know: "¿Sabías que...?"
145 + did_you_know_text: "RustWarden es un gestor de contraseñas seguro escrito en Rust para ofrecer mejor rendimiento y seguridad mejorada. ¡Mantente atento a las nuevas funcionalidades!"
146 +
74 147 # Mensajes comunes
75 148 common:
76 149 welcome: "Bienvenido {username}"
@@ -154,8 +227,120 @@ breadcrumb:
154 227 password: "Contraseña"
155 228 sessions: "Sesiones"
156 229
157 -# Configuración de administración
230 +# Administración
158 231 admin:
232 + dashboard:
233 + title: "Administración"
234 + heading: "Panel de Administración"
235 + users_card_title: "Usuarios"
236 + users_card_subtitle: "Usuarios totales"
237 + access_button: "Acceder"
238 + sessions_card_title: "Sesiones activas"
239 + sessions_card_subtitle: "Sesiones en curso"
240 + admins_card_title: "Administradores"
241 + admins_card_subtitle: "Cuentas admin"
242 + last_activity_card_title: "Última actividad"
243 + last_activity_card_subtitle: "Último inicio de sesión"
244 + audit_section_title: "Estadísticas de logs de auditoría"
245 + audit_total_logs: "Logs totales"
246 + audit_last_24h: "Últimas 24h"
247 + audit_by_level: "Por nivel"
248 + chart_label: "Logs por nivel"
249 + settings_card_title: "Configuración de la Aplicación"
250 + settings_card_desc: "Configurar los parámetros globales de la aplicación"
251 + apps_card_title: "Gestión de Aplicaciones"
252 + apps_card_desc: "Activar o desactivar aplicaciones modulares"
253 + orgs_card_title: "Gestión de Organizaciones"
254 + orgs_card_desc: "Crear y gestionar organizaciones y sus miembros"
255 + orgs_stat_title: "Organizaciones"
256 + orgs_members_stat_title: "Miembros totales"
257 + errors:
258 + title: "Códigos de Error"
259 + description: "Documentación de los códigos de error de la aplicación. Cada código de error está asociado a un middleware y proporciona información de contexto para la depuración."
260 + breadcrumb_errors: "Códigos de Error"
261 + table_code: "Código"
262 + table_description: "Descripción"
263 + table_category: "Categoría"
264 + table_middleware: "Middleware"
265 + table_context: "Contexto"
266 + table_example: "Ejemplo"
267 + legend_title: "Leyenda"
268 + legend_code: "Identificador único del error (formato: CATEGORY_NUMBER)"
269 + legend_description: "Descripción del error"
270 + legend_category: "Tipo de error (Autenticación, CSRF, Rate Limiting, etc.)"
271 + legend_middleware: "Middleware responsable de generar este error"
272 + legend_context: "Explicación detallada de cuándo ocurre este error"
273 + legend_example: "Ejemplo concreto de una situación que genera este error"
274 + error_auth001_desc: "Credenciales inválidas"
275 + error_auth001_category: "Autenticación"
276 + error_auth001_context: "El usuario proporcionó credenciales incorrectas al iniciar sesión"
277 + error_auth001_example: "Intento de inicio de sesión con contraseña incorrecta"
278 + error_auth002_desc: "Token inválido"
279 + error_auth002_category: "Autenticación"
280 + error_auth002_context: "El token JWT proporcionado es inválido o está malformado"
281 + error_auth002_example: "Token JWT corrupto o firmado con una clave diferente"
282 + error_auth003_desc: "Token expirado"
283 + error_auth003_category: "Autenticación"
284 + error_auth003_context: "El token JWT ha expirado y debe renovarse"
285 + error_auth003_example: "Intento de acceso con un token expirado después del período de validez"
286 + error_csrf001_desc: "Token CSRF faltante"
287 + error_csrf001_category: "Protección CSRF"
288 + error_csrf001_context: "La solicitud POST/PUT/DELETE no contiene un token CSRF"
289 + error_csrf001_example: "Formulario enviado sin token CSRF en el header o body"
290 + error_csrf002_desc: "Token CSRF inválido"
291 + error_csrf002_category: "Protección CSRF"
292 + error_csrf002_context: "El token CSRF proporcionado no coincide con el token de sesión"
293 + error_csrf002_example: "Token CSRF modificado o robado"
294 + error_rate001_desc: "Límite de tasa excedido"
295 + error_rate001_category: "Rate Limiting"
296 + error_rate001_context: "Se ha excedido el número de solicitudes por minuto"
297 + error_rate001_example: "Más de 100 solicitudes/min en endpoints admin"
298 + error_valid001_desc: "Datos de entrada inválidos"
299 + error_valid001_category: "Validación"
300 + error_valid001_context: "Los datos proporcionados no cumplen con las reglas de validación"
301 + error_valid001_example: "Formato de email inválido o campo requerido faltante"
302 + error_valid002_desc: "Complejidad de contraseña no cumplida"
303 + error_valid002_category: "Validación"
304 + error_valid002_context: "La contraseña no cumple con las reglas de complejidad configuradas"
305 + error_valid002_example: "Contraseña demasiado corta o sin los caracteres requeridos"
306 + error_rbac001_desc: "Permisos insuficientes"
307 + error_rbac001_category: "Autorización"
308 + error_rbac001_context: "El usuario no tiene los permisos necesarios para esta acción"
309 + error_rbac001_example: "Usuario no administrador intentando acceder a /admin/users"
310 + error_rbac002_desc: "Rol de administrador requerido"
311 + error_rbac002_category: "Autorización"
312 + error_rbac002_context: "La acción requiere un rol de administrador"
313 + error_rbac002_example: "Intento de eliminar un usuario sin rol admin"
314 + error_db001_desc: "Error de operación de base de datos"
315 + error_db001_category: "Base de datos"
316 + error_db001_context: "Una operación de base de datos falló"
317 + error_db001_example: "Restricción de clave foránea violada o conexión DB perdida"
318 + error_db002_desc: "Recurso no encontrado"
319 + error_db002_category: "Base de datos"
320 + error_db002_context: "El recurso solicitado no existe en la base de datos"
321 + error_db002_example: "Usuario con ID inexistente"
322 + user_form:
323 + breadcrumb_create: "Crear"
324 + breadcrumb_edit: "Modificar"
325 + title_create: "Crear un usuario"
326 + title_edit: "Modificar un usuario"
327 + back_to_list: "Volver a la lista"
328 + username_label: "Nombre de usuario"
329 + username_placeholder: "nombre_usuario"
330 + username_readonly_hint: "El nombre de usuario no se puede cambiar después de la creación"
331 + email_label: "Email"
332 + email_placeholder: "usuario@example.com"
333 + password_label: "Contraseña"
334 + password_optional: "Opcional"
335 + password_placeholder: "Dejar vacío para generar una contraseña aleatoria"
336 + password_hint: "Si se deja vacío, se generará automáticamente una contraseña segura"
337 + roles_label: "Roles"
338 + roles_hint: "Seleccione al menos un rol"
339 + must_change_password_label: "Forzar cambio de contraseña"
340 + must_change_password_desc: "El usuario deberá cambiar su contraseña en el próximo inicio de sesión"
341 + cancel: "Cancelar"
342 + submit_create: "Crear usuario"
343 + submit_edit: "Guardar cambios"
159 344 settings:
160 345 title: "Configuración de la aplicación"
161 346 page_title: "Configuración de la aplicación - Admin - RustCloud"
@@ -174,6 +359,34 @@ admin:
174 359 saved: "Configuración guardada con éxito"
175 360 error: "Error al guardar la configuración"
176 361 network_error: "Error de red. Por favor intente nuevamente."
362 + password_security: "Seguridad de Contraseñas"
363 + password_min_length: "Longitud mínima"
364 + password_min_length_hint: "Entre 6 y 128 caracteres"
365 + password_complexity: "Requisitos de complejidad"
366 + require_uppercase: "Mayúscula requerida"
367 + require_lowercase: "Minúscula requerida"
368 + require_digits: "Dígito requerido"
369 + require_special: "Carácter especial requerido"
370 + password_expiration: "Expiración de contraseñas"
371 + password_expiration_desc: "Forzar cambio periódico de contraseña"
372 + expiration_days: "Expiración después de (días)"
373 + expiration_alert: "Alerta por email antes de la expiración"
374 + expiration_alert_days: "Alerta (días antes)"
375 + audit_logging: "Registro de Auditoría"
376 + log_level: "Nivel de log"
377 + log_level_debug: "DEBUG - Todos los detalles"
378 + log_level_info: "INFO - Información normal"
379 + log_level_warn: "WARN - Solo advertencias"
380 + log_level_error: "ERROR - Solo errores"
381 + log_level_hint: "Controla la verbosidad de los logs de auditoría"
382 + log_retention: "Retención de logs (días)"
383 + log_retention_hint: "Período de retención de logs (1-365 días)"
384 + audit_info_message: "Los logs de auditoría registran todas las acciones administrativas (creación, modificación, eliminación de usuarios)."
385 + save_changes: "Guardar cambios"
386 + save_changes_desc: "La configuración se aplicará inmediatamente"
387 + saving: "Guardando..."
388 + save_success: "¡Configuración guardada con éxito!"
389 + save_error_prefix: "Error"
177 390 users_list:
178 391 page_title: "Gestión de Usuarios - Admin - RustCloud"
179 392 title: "Gestión de Usuarios"
@@ -211,8 +424,21 @@ admin:
211 424 delete_confirm_email: "Email:"
212 425 delete_irreversible: "¡Esta acción es irreversible!"
213 426 delete_button: "Eliminar permanentemente"
427 + delete: "Eliminar"
214 428 delete_error: "Error al eliminar el usuario"
215 429 network_error: "Error de red"
430 + edit_user_title: "Modificar usuario"
431 + resetting: "Restableciendo..."
432 + reset_by_email_title: "Restablecer por email"
433 + reset_email: "Restablecer por email"
434 + close: "Cerrar"
435 + password_reset_success_title: "Contraseña restablecida"
436 + password_warning_once: "¡Esta contraseña solo se mostrará una vez. Cópiela ahora!"
437 + new_password_label: "Nueva contraseña"
438 + copy: "Copiar"
439 + password_copied: "Contraseña copiada al portapapeles"
440 + password_set_success: "Contraseña restablecida con éxito"
441 + reset_error: "Error al restablecer la contraseña"
216 442
217 443 # Cambio de contraseña
218 444 password:
M locales/fr.yml
+225 -0
@@ -34,8 +34,41 @@ login:
34 34 resend_prompt: "Veuillez renseigner votre email avant de renvoyer."
35 35 register_success: "Compte créé avec succès ! Veuillez vérifier votre email."
36 36 register_error: "Erreur lors de l'inscription"
37 + email_label: "Email"
37 38 footer_text: "Fork de Nextcloud en Rust"
38 39
40 +# Renvoyer l'email de validation
41 +resend_verification:
42 + page_title: "Renvoyer l'email de validation - RustWarden"
43 + title: "Renvoyer l'email de validation"
44 + subtitle: "Entrez votre adresse email pour recevoir un nouveau lien de validation."
45 + email_label: "Adresse email"
46 + email_placeholder: "vous@example.com"
47 + submit: "Envoyer le lien de validation"
48 + back_to_login: "Revenir à la connexion"
49 + error_default: "Impossible d'envoyer la demande."
50 + success_default: "Si le compte existe et n'est pas vérifié, un email a été envoyé."
51 + error_missing_email: "Veuillez renseigner votre email."
52 + error_cannot_send: "Impossible d'envoyer la demande pour l'instant."
53 +
54 +# Réinitialisation de mot de passe (via lien email)
55 +password_reset:
56 + page_title: "Réinitialiser le mot de passe"
57 + title: "Réinitialiser le mot de passe"
58 + subtitle: "Entrez votre nouveau mot de passe ci-dessous."
59 + new_password_label: "Nouveau mot de passe"
60 + new_password_placeholder: "Nouveau mot de passe sécurisé"
61 + requirements: "⚠️ Doit contenir : 8+ caractères, majuscule, minuscule, chiffre, caractère spécial"
62 + confirm_password_label: "Confirmer le mot de passe"
63 + confirm_password_placeholder: "Confirmez votre nouveau mot de passe"
64 + missing_token_url: "Token de réinitialisation manquant dans l'URL."
65 + missing_token: "Token de réinitialisation manquant."
66 + missing_new_password: "Veuillez entrer un nouveau mot de passe."
67 + passwords_not_match: "Les mots de passe ne correspondent pas."
68 + success_message: "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter."
69 + error_link_invalid: "Impossible de réinitialiser le mot de passe. Le lien peut être expiré ou invalide."
70 + error_generic: "Impossible de réinitialiser le mot de passe pour l'instant."
71 +
39 72 # Messages d'erreur
40 73 error:
41 74 invalid_credentials: "Identifiants incorrects"
@@ -66,11 +99,51 @@ error:
66 99 # Paramètres
67 100 settings:
68 101 title: "Paramètres"
102 + page_title: "Paramètres - RustWarden"
103 + subtitle: "Gérez vos préférences et paramètres de compte"
104 + profile_title: "Mon Profil"
105 + profile_desc: "Modifiez vos informations personnelles (nom d'utilisateur, email)"
106 + password_title: "Mot de passe"
107 + password_desc: "Changez votre mot de passe pour sécuriser votre compte"
108 + sessions_title: "Sessions actives"
109 + sessions_desc: "Gérez vos sessions actives et révoquez l'accès depuis d'autres appareils"
110 + preferences_title: "Préférences"
111 + preferences_desc: "Personnalisez votre expérience (thème, langue)"
112 + access: "Accéder"
113 + info_title: "Informations"
114 + info_desc: "Utilisez les sections ci-dessus pour gérer différents aspects de votre compte. Toutes les modifications sont sécurisées et nécessitent votre authentification."
69 115 language: "Langue"
70 116 language_description: "Choisissez votre langue préférée"
71 117 save: "Enregistrer"
72 118 saved: "Paramètres enregistrés"
73 119
120 +# Page d'accueil
121 +home:
122 + welcome: "Bienvenue, {username} !"
123 + subtitle: "Bienvenue sur votre espace personnel"
124 + my_account: "Mon Compte"
125 + upcoming_features: "Fonctionnalités à Venir"
126 + stat_username: "Nom d'utilisateur"
127 + stat_email: "Email"
128 + stat_member_since: "Membre depuis"
129 + stat_roles: "Rôles"
130 + feature_secrets_name: "Gestion des secrets"
131 + feature_secrets_desc: "Stockage sécurisé de mots de passe et secrets"
132 + feature_2fa_name: "Authentification 2FA"
133 + feature_2fa_desc: "Authentification à deux facteurs"
134 + feature_files_name: "Gestionnaire de Fichiers"
135 + feature_files_desc: "Parcourir, créer, renommer et supprimer vos fichiers et dossiers"
136 + feature_upload_name: "Upload/Download"
137 + feature_upload_desc: "Télécharger et téléverser des fichiers avec barre de progression"
138 + feature_webdav_name: "WebDAV et Synchronisation"
139 + feature_webdav_desc: "Synchroniser vos fichiers avec clients desktop via WebDAV"
140 + feature_mobile_name: "API Mobile"
141 + feature_mobile_desc: "Synchronisation avec applications mobiles (iOS/Android)"
142 + badge_soon: "Bientôt"
143 + badge_planned: "Prévu"
144 + did_you_know: "Le Saviez-Vous ?"
145 + did_you_know_text: "RustWarden est un gestionnaire de mots de passe sécurisé écrit en Rust pour offrir de meilleures performances et une sécurité renforcée. Restez à l'écoute pour les nouvelles fonctionnalités !"
146 +
74 147 # Messages généraux
75 148 common:
76 149 welcome: "Bienvenue {username}"
@@ -158,10 +231,123 @@ breadcrumb:
158 231 preferences: "Préférences"
159 232 profile: "Profil"
160 233 password: "Mot de passe"
234 + users: "Utilisateurs"
161 235 sessions: "Sessions"
162 236
163 237 # Admin Settings
164 238 admin:
239 + dashboard:
240 + title: "Administration"
241 + heading: "Tableau de bord Administrateur"
242 + users_card_title: "Utilisateurs"
243 + users_card_subtitle: "Total utilisateurs"
244 + access_button: "Accéder"
245 + sessions_card_title: "Sessions actives"
246 + sessions_card_subtitle: "Sessions en cours"
247 + admins_card_title: "Administrateurs"
248 + admins_card_subtitle: "Comptes admin"
249 + last_activity_card_title: "Dernière activité"
250 + last_activity_card_subtitle: "Dernière connexion"
251 + audit_section_title: "Statistiques des logs d'audit"
252 + audit_total_logs: "Total des logs"
253 + audit_last_24h: "Dernières 24h"
254 + audit_by_level: "Par niveau"
255 + chart_label: "Logs par niveau"
256 + settings_card_title: "Paramètres Application"
257 + settings_card_desc: "Configurer les paramètres globaux de l'application"
258 + apps_card_title: "Gestion des Applications"
259 + apps_card_desc: "Activer ou désactiver les applications modulaires"
260 + orgs_card_title: "Gestion des Organisations"
261 + orgs_card_desc: "Créer et gérer les organisations et leurs membres"
262 + orgs_stat_title: "Organisations"
263 + orgs_members_stat_title: "Membres totaux"
264 + errors:
265 + title: "Codes d'Erreur"
266 + description: "Documentation des codes d'erreur de l'application. Chaque code d'erreur est associé à un middleware et fournit des informations de contexte pour le débogage."
267 + breadcrumb_errors: "Codes d'Erreur"
268 + table_code: "Code"
269 + table_description: "Description"
270 + table_category: "Catégorie"
271 + table_middleware: "Middleware"
272 + table_context: "Contexte"
273 + table_example: "Exemple"
274 + legend_title: "Légende"
275 + legend_code: "Identifiant unique de l'erreur (format: CATEGORY_NUMBER)"
276 + legend_description: "Description en français de l'erreur"
277 + legend_category: "Type d'erreur (Authentification, CSRF, Rate Limiting, etc.)"
278 + legend_middleware: "Middleware responsable de générer cette erreur"
279 + legend_context: "Explication détaillée de quand cette erreur se produit"
280 + legend_example: "Exemple concret de situation générant cette erreur"
281 + error_auth001_desc: "Identifiants invalides"
282 + error_auth001_category: "Authentification"
283 + error_auth001_context: "L'utilisateur a fourni des identifiants incorrects lors de la connexion"
284 + error_auth001_example: "Tentative de connexion avec un mot de passe incorrect"
285 + error_auth002_desc: "Token invalide"
286 + error_auth002_category: "Authentification"
287 + error_auth002_context: "Le token JWT fourni est invalide ou malformé"
288 + error_auth002_example: "Token JWT corrompu ou signé avec une clé différente"
289 + error_auth003_desc: "Token expiré"
290 + error_auth003_category: "Authentification"
291 + error_auth003_context: "Le token JWT a expiré et doit être renouvelé"
292 + error_auth003_example: "Tentative d'accès avec un token expiré après la durée de validité"
293 + error_csrf001_desc: "Token CSRF manquant"
294 + error_csrf001_category: "CSRF Protection"
295 + error_csrf001_context: "La requête POST/PUT/DELETE ne contient pas de token CSRF"
296 + error_csrf001_example: "Formulaire soumis sans token CSRF dans le header ou le body"
297 + error_csrf002_desc: "Token CSRF invalide"
298 + error_csrf002_category: "CSRF Protection"
299 + error_csrf002_context: "Le token CSRF fourni ne correspond pas au token de la session"
300 + error_csrf002_example: "Token CSRF modifié ou volé"
301 + error_rate001_desc: "Limite de taux dépassée"
302 + error_rate001_category: "Rate Limiting"
303 + error_rate001_context: "Le nombre de requêtes par minute a été dépassé"
304 + error_rate001_example: "Plus de 100 requêtes/min sur les endpoints admin"
305 + error_valid001_desc: "Données d'entrée invalides"
306 + error_valid001_category: "Validation"
307 + error_valid001_context: "Les données fournies ne respectent pas les règles de validation"
308 + error_valid001_example: "Email au format invalide ou champ requis manquant"
309 + error_valid002_desc: "Complexité du mot de passe non respectée"
310 + error_valid002_category: "Validation"
311 + error_valid002_context: "Le mot de passe ne respecte pas les règles de complexité configurées"
312 + error_valid002_example: "Mot de passe trop court ou ne contenant pas les caractères requis"
313 + error_rbac001_desc: "Permissions insuffisantes"
314 + error_rbac001_category: "Autorisation"
315 + error_rbac001_context: "L'utilisateur n'a pas les permissions nécessaires pour cette action"
316 + error_rbac001_example: "Utilisateur non-admin tentant d'accéder à /admin/users"
317 + error_rbac002_desc: "Rôle administrateur requis"
318 + error_rbac002_category: "Autorisation"
319 + error_rbac002_context: "L'action nécessite le rôle administrateur"
320 + error_rbac002_example: "Tentative de suppression d'utilisateur sans rôle admin"
321 + error_db001_desc: "Erreur d'opération de base de données"
322 + error_db001_category: "Base de données"
323 + error_db001_context: "Une opération de base de données a échoué"
324 + error_db001_example: "Contrainte de clé étrangère violée ou connexion DB perdue"
325 + error_db002_desc: "Ressource non trouvée"
326 + error_db002_category: "Base de données"
327 + error_db002_context: "La ressource demandée n'existe pas dans la base de données"
328 + error_db002_example: "Utilisateur avec ID inexistant"
329 + user_form:
330 + breadcrumb_create: "Créer"
331 + breadcrumb_edit: "Modifier"
332 + title_create: "Créer un utilisateur"
333 + title_edit: "Modifier un utilisateur"
334 + back_to_list: "Retour à la liste"
335 + username_label: "Nom d'utilisateur"
336 + username_placeholder: "nom_utilisateur"
337 + username_readonly_hint: "Le nom d'utilisateur ne peut pas être modifié après création"
338 + email_label: "Email"
339 + email_placeholder: "utilisateur@example.com"
340 + password_label: "Mot de passe"
341 + password_optional: "Optionnel"
342 + password_placeholder: "Laisser vide pour générer un mot de passe aléatoire"
343 + password_hint: "Si laissé vide, un mot de passe sécurisé sera généré automatiquement"
344 + roles_label: "Rôles"
345 + roles_hint: "Sélectionnez au moins un rôle"
346 + must_change_password_label: "Forcer le changement de mot de passe"
347 + must_change_password_desc: "L'utilisateur devra changer son mot de passe à la prochaine connexion"
348 + cancel: "Annuler"
349 + submit_create: "Créer l'utilisateur"
350 + submit_edit: "Enregistrer les modifications"
165 351 settings:
166 352 title: "Paramètres de l'Application"
167 353 page_title: "Paramètres de l'Application - Admin - RustCloud"
@@ -180,6 +366,34 @@ admin:
180 366 saved: "Paramètres enregistrés avec succès"
181 367 error: "Erreur lors de l'enregistrement"
182 368 network_error: "Erreur réseau. Veuillez réessayer."
369 + password_security: "Sécurité des Mots de Passe"
370 + password_min_length: "Longueur minimale"
371 + password_min_length_hint: "Entre 6 et 128 caractères"
372 + password_complexity: "Exigences de complexité"
373 + require_uppercase: "Majuscule requise"
374 + require_lowercase: "Minuscule requise"
375 + require_digits: "Chiffre requis"
376 + require_special: "Caractère spécial requis"
377 + password_expiration: "Expiration des mots de passe"
378 + password_expiration_desc: "Forcer le changement périodique"
379 + expiration_days: "Expiration après (jours)"
380 + expiration_alert: "Alerte email avant expiration"
381 + expiration_alert_days: "Alerte (jours avant)"
382 + audit_logging: "Journalisation des Audits"
383 + log_level: "Niveau de log"
384 + log_level_debug: "DEBUG - Tous les détails"
385 + log_level_info: "INFO - Informations normales"
386 + log_level_warn: "WARN - Avertissements uniquement"
387 + log_level_error: "ERROR - Erreurs uniquement"
388 + log_level_hint: "Contrôle la verbosité des logs d'audit"
389 + log_retention: "Rétention des logs (jours)"
390 + log_retention_hint: "Durée de conservation des logs (1-365 jours)"
391 + audit_info_message: "Les logs d'audit enregistrent toutes les actions administratives (création, modification, suppression d'utilisateurs)."
392 + save_changes: "Sauvegarder les modifications"
393 + save_changes_desc: "Les paramètres seront appliqués immédiatement"
394 + saving: "Enregistrement..."
395 + save_success: "Paramètres enregistrés avec succès !"
396 + save_error_prefix: "Erreur"
183 397 users_list:
184 398 page_title: "Gestion des Utilisateurs - Admin - RustCloud"
185 399 title: "Gestion des Utilisateurs"
@@ -220,8 +434,19 @@ admin:
220 434 delete_confirm_email: "Email :"
221 435 delete_irreversible: "Cette action est irréversible !"
222 436 delete_button: "Supprimer définitivement"
437 + edit_user_title: "Modifier l'utilisateur"
223 438 delete_error: "Erreur lors de la suppression de l'utilisateur"
224 439 network_error: "Erreur réseau"
440 + resetting: "Réinitialisation..."
441 + reset_by_email_title: "Réinitialisation par email"
442 + close: "Fermer"
443 + password_reset_success_title: "Mot de passe réinitialisé"
444 + password_warning_once: "Ce mot de passe ne sera affiché qu'une seule fois. Copiez-le maintenant !"
445 + new_password_label: "Nouveau mot de passe"
446 + copy: "Copier"
447 + password_copied: "Mot de passe copié dans le presse-papiers"
448 + password_set_success: "Mot de passe réinitialisé avec succès"
449 + reset_error: "Erreur lors de la réinitialisation du mot de passe"
225 450
226 451 # Password Change
227 452 password:
M src/api/handlers/admin_handler.rs
+459 -213
@@ -9,17 +9,36 @@ use axum::{
9 9 };
10 10 use sea_orm::DatabaseConnection;
11 11 use serde::Deserialize;
12 +use std::sync::Arc;
12 13 use tower_sessions::Session;
13 14
14 15 use crate::api::middleware::csrf;
15 16 use crate::auth::jwt::Claims;
16 17 use crate::error::AppError;
18 +use crate::i18n::extractor::Locale;
19 +use crate::i18n::service::I18nService;
17 20 use crate::impl_template_response;
18 21 use crate::models::{role, user_role};
22 +use crate::services::app_settings_service::AppSettingsService;
19 23 use crate::templates::helpers::build_base_context_with_claims;
20 24 use axum::extract::Extension;
21 25 use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
22 26
27 +/// Load app_domain and app_description from the database with sane defaults.
28 +async fn load_app_domain_and_description(
29 + db: &DatabaseConnection,
30 +) -> Result<(String, String), AppError> {
31 + let app_domain = AppSettingsService::get_setting(db, "app_domain")
32 + .await?
33 + .map(|s| s.value)
34 + .unwrap_or_else(|| "localhost".to_string());
35 + let app_description = AppSettingsService::get_setting(db, "app_description")
36 + .await?
37 + .map(|s| s.value)
38 + .unwrap_or_else(|| "Secure password manager".to_string());
39 + Ok((app_domain, app_description))
40 +}
41 +
23 42 /// Inline DTO: Create user request from admin form
24 43 #[derive(Debug, Clone, serde::Deserialize)]
25 44 pub struct CreateUserRequest {
@@ -122,6 +141,32 @@ struct AdminDashboardTemplate {
122 141 user_menu_profile: String,
123 142 user_menu_logout: String,
124 143 user_menu_settings: String,
144 + // Translated strings
145 + breadcrumb_home: String,
146 + breadcrumb_admin: String,
147 + heading: String,
148 + users_card_title: String,
149 + users_card_subtitle: String,
150 + access_button: String,
151 + sessions_card_title: String,
152 + sessions_card_subtitle: String,
153 + admins_card_title: String,
154 + admins_card_subtitle: String,
155 + last_activity_card_title: String,
156 + last_activity_card_subtitle: String,
157 + audit_section_title: String,
158 + audit_total_logs_label: String,
159 + audit_last_24h: String,
160 + audit_by_level: String,
161 + chart_label: String,
162 + settings_card_title: String,
163 + settings_card_desc: String,
164 + apps_card_title: String,
165 + apps_card_desc: String,
166 + orgs_card_title: String,
167 + orgs_card_desc: String,
168 + orgs_stat_title: String,
169 + orgs_members_stat_title: String,
125 170 }
126 171
127 172 crate::impl_template_response!(AdminDashboardTemplate);
@@ -129,11 +174,15 @@ crate::impl_template_response!(AdminDashboardTemplate);
129 174 /// Handler for GET /admin - Admin Dashboard (PROTECTED - Admin only)
130 175 pub async fn admin_dashboard(
131 176 Extension(claims): Extension<Claims>,
177 + Extension(i18n_service): Extension<Arc<I18nService>>,
178 + Extension(locale): Extension<Locale>,
132 179 State(db): State<DatabaseConnection>,
133 180 session: Session,
134 181 ) -> Result<impl IntoResponse, AppError> {
182 + let locale_str = locale.as_str();
135 183 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
136 184 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
185 + let (app_domain, app_description) = load_app_domain_and_description(&db).await?;
137 186
138 187 // Compute basic stats from DB
139 188 use crate::models::user;
@@ -160,12 +209,12 @@ pub async fn admin_dashboard(
160 209 };
161 210
162 211 let template = AdminDashboardTemplate {
163 - page_title: "Administration".to_string(),
212 + page_title: i18n_service.translate("admin.dashboard.title", locale_str).await,
164 213 csrf_token,
165 214 app_name: base_context.app_name,
166 215 app_version: base_context.app_version,
167 - app_domain: "localhost".to_string(),
168 - app_description: "Secure password manager".to_string(),
216 + app_domain,
217 + app_description,
169 218 username: base_context.username,
170 219 email: base_context.email,
171 220 initials: base_context.initials,
@@ -184,9 +233,34 @@ pub async fn admin_dashboard(
184 233 audit_info_count: 0,
185 234 audit_warn_count: 0,
186 235 audit_error_count: 0,
187 - user_menu_profile: "Profil".to_string(),
188 - user_menu_logout: "Déconnexion".to_string(),
189 - user_menu_settings: "Paramètres".to_string(),
236 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
237 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
238 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
239 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
240 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
241 + heading: i18n_service.translate("admin.dashboard.heading", locale_str).await,
242 + users_card_title: i18n_service.translate("admin.dashboard.users_card_title", locale_str).await,
243 + users_card_subtitle: i18n_service.translate("admin.dashboard.users_card_subtitle", locale_str).await,
244 + access_button: i18n_service.translate("admin.dashboard.access_button", locale_str).await,
245 + sessions_card_title: i18n_service.translate("admin.dashboard.sessions_card_title", locale_str).await,
246 + sessions_card_subtitle: i18n_service.translate("admin.dashboard.sessions_card_subtitle", locale_str).await,
247 + admins_card_title: i18n_service.translate("admin.dashboard.admins_card_title", locale_str).await,
248 + admins_card_subtitle: i18n_service.translate("admin.dashboard.admins_card_subtitle", locale_str).await,
249 + last_activity_card_title: i18n_service.translate("admin.dashboard.last_activity_card_title", locale_str).await,
250 + last_activity_card_subtitle: i18n_service.translate("admin.dashboard.last_activity_card_subtitle", locale_str).await,
251 + audit_section_title: i18n_service.translate("admin.dashboard.audit_section_title", locale_str).await,
252 + audit_total_logs_label: i18n_service.translate("admin.dashboard.audit_total_logs", locale_str).await,
253 + audit_last_24h: i18n_service.translate("admin.dashboard.audit_last_24h", locale_str).await,
254 + audit_by_level: i18n_service.translate("admin.dashboard.audit_by_level", locale_str).await,
255 + chart_label: i18n_service.translate("admin.dashboard.chart_label", locale_str).await,
256 + settings_card_title: i18n_service.translate("admin.dashboard.settings_card_title", locale_str).await,
257 + settings_card_desc: i18n_service.translate("admin.dashboard.settings_card_desc", locale_str).await,
258 + apps_card_title: i18n_service.translate("admin.dashboard.apps_card_title", locale_str).await,
259 + apps_card_desc: i18n_service.translate("admin.dashboard.apps_card_desc", locale_str).await,
260 + orgs_card_title: i18n_service.translate("admin.dashboard.orgs_card_title", locale_str).await,
261 + orgs_card_desc: i18n_service.translate("admin.dashboard.orgs_card_desc", locale_str).await,
262 + orgs_stat_title: i18n_service.translate("admin.dashboard.orgs_stat_title", locale_str).await,
263 + orgs_members_stat_title: i18n_service.translate("admin.dashboard.orgs_members_stat_title", locale_str).await,
190 264 };
191 265
192 266 Ok(template)
@@ -262,7 +336,7 @@ struct UserListTemplate {
262 336 user_menu_profile: String,
263 337 user_menu_logout: String,
264 338 user_menu_settings: String,
265 - // Translated strings (hardcoded FR)
339 + // Translated strings
266 340 title: String,
267 341 breadcrumb_home: String,
268 342 breadcrumb_admin: String,
@@ -306,6 +380,17 @@ struct UserListTemplate {
306 380 delete_error: String,
307 381 network_error: String,
308 382 cancel: String,
383 + // JS modal strings
384 + resetting: String,
385 + reset_by_email_title: String,
386 + close: String,
387 + password_reset_success_title: String,
388 + password_warning_once: String,
389 + new_password_label: String,
390 + copy: String,
391 + password_copied: String,
392 + password_set_success: String,
393 + reset_error: String,
309 394 }
310 395
311 396 crate::impl_template_response!(UserListTemplate);
@@ -336,6 +421,28 @@ struct UserFormTemplate {
336 421 user_menu_profile: String,
337 422 user_menu_logout: String,
338 423 user_menu_settings: String,
424 + // Translated strings
425 + breadcrumb_home: String,
426 + breadcrumb_admin: String,
427 + breadcrumb_users: String,
428 + breadcrumb_mode: String,
429 + form_title: String,
430 + back_to_list: String,
431 + username_label: String,
432 + username_placeholder: String,
433 + username_readonly_hint: String,
434 + email_label: String,
435 + email_placeholder: String,
436 + password_label: String,
437 + password_optional: String,
438 + password_placeholder: String,
439 + password_hint: String,
440 + roles_label: String,
441 + roles_hint: String,
442 + must_change_password_label: String,
443 + must_change_password_desc: String,
444 + cancel: String,
445 + submit_label: String,
339 446 }
340 447
341 448 crate::impl_template_response!(UserFormTemplate);
@@ -345,10 +452,13 @@ crate::impl_template_response!(UserFormTemplate);
345 452 /// Displays a paginated list of users with search and role filtering.
346 453 pub async fn list_users(
347 454 Extension(claims): Extension<Claims>,
455 + Extension(i18n_service): Extension<Arc<I18nService>>,
456 + Extension(locale): Extension<Locale>,
348 457 State(db): State<DatabaseConnection>,
349 458 session: Session,
350 459 Query(params): Query<UserListQuery>,
351 460 ) -> Result<impl IntoResponse, AppError> {
461 + let locale_str = locale.as_str();
352 462 let page = params.page.unwrap_or(1);
353 463 let per_page = params.per_page.unwrap_or(50);
354 464 let search = params.search;
@@ -394,6 +504,7 @@ pub async fn list_users(
394 504
395 505 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
396 506 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
507 + let (app_domain, app_description) = load_app_domain_and_description(&db).await?;
397 508
398 509 let mut users_display = Vec::new();
399 510 for u in page_users {
@@ -435,21 +546,32 @@ pub async fn list_users(
435 546 });
436 547 }
437 548
438 - let results_summary = format!("{} utilisateur(s) sur {}", users_display.len(), total);
549 + let results_summary_tpl = i18n_service
550 + .translate("admin.users_list.results_summary", locale_str)
551 + .await;
552 + let results_summary = results_summary_tpl
553 + .replace("{count}", &users_display.len().to_string())
554 + .replace("{total}", &total.to_string());
555 +
439 556 let page_info = if total_pages > 1 {
440 - format!("Page {} sur {}", page, total_pages)
557 + let page_info_tpl = i18n_service
558 + .translate("admin.users_list.page_info", locale_str)
559 + .await;
560 + page_info_tpl
561 + .replace("{page}", &page.to_string())
562 + .replace("{total_pages}", &total_pages.to_string())
441 563 } else {
442 564 String::new()
443 565 };
444 566
445 567 let template = UserListTemplate {
446 - page_title: "Gestion des utilisateurs".to_string(),
568 + page_title: i18n_service.translate("admin.users_list.page_title", locale_str).await,
447 569 csrf_token,
448 570 app_name: base_context.app_name,
449 - locale: "fr".to_string(),
571 + locale: locale_str.to_string(),
450 572 app_version: base_context.app_version,
451 - app_domain: "localhost".to_string(),
452 - app_description: "Secure password manager".to_string(),
573 + app_domain,
574 + app_description,
453 575 username: base_context.username,
454 576 email: base_context.email,
455 577 initials: base_context.initials,
@@ -466,52 +588,62 @@ pub async fn list_users(
466 588 has_role_filter: role_filter.is_some() && !role_filter.as_ref().unwrap().is_empty(),
467 589 is_admin_filter: role_filter.as_ref().map(|r| r == "admin").unwrap_or(false),
468 590 is_user_filter: role_filter.as_ref().map(|r| r == "user").unwrap_or(false),
469 - title: "Utilisateurs".to_string(),
470 - breadcrumb_home: "Accueil".to_string(),
471 - breadcrumb_admin: "Administration".to_string(),
472 - breadcrumb_users: "Utilisateurs".to_string(),
473 - create_user: "Créer un utilisateur".to_string(),
474 - search_label: "Rechercher".to_string(),
475 - search_placeholder: "Nom d'utilisateur ou email...".to_string(),
476 - role_filter_label: "Filtrer par rôle".to_string(),
477 - all_roles: "Tous les rôles".to_string(),
478 - per_page_label: "Par page".to_string(),
479 - search_button: "Rechercher".to_string(),
591 + title: i18n_service.translate("admin.users_list.title", locale_str).await,
592 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
593 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
594 + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await,
595 + create_user: i18n_service.translate("admin.users_list.create_user", locale_str).await,
596 + search_label: i18n_service.translate("admin.users_list.search_label", locale_str).await,
597 + search_placeholder: i18n_service.translate("admin.users_list.search_placeholder", locale_str).await,
598 + role_filter_label: i18n_service.translate("admin.users_list.role_filter_label", locale_str).await,
599 + all_roles: i18n_service.translate("admin.users_list.all_roles", locale_str).await,
600 + per_page_label: i18n_service.translate("admin.users_list.per_page_label", locale_str).await,
601 + search_button: i18n_service.translate("admin.users_list.search_button", locale_str).await,
480 602 results_summary,
481 603 page_info,
482 - table_username: "Nom d'utilisateur".to_string(),
483 - table_email: "Email".to_string(),
484 - table_roles: "Rôles".to_string(),
485 - table_created_at: "Créé le".to_string(),
486 - table_last_login: "Dernière connexion".to_string(),
487 - table_actions: "Actions".to_string(),
488 - never: "Jamais".to_string(),
489 - edit: "Modifier".to_string(),
490 - delete: "Supprimer".to_string(),
491 - reset_password: "Réinitialiser le mot de passe".to_string(),
492 - no_users: "Aucun utilisateur trouvé".to_string(),
493 - reset_password_title: "Réinitialiser le mot de passe".to_string(),
494 - reset_generate: "Générer un mot de passe".to_string(),
495 - reset_custom: "Mot de passe personnalisé".to_string(),
496 - reset_email: "Envoyer par email".to_string(),
497 - reset_new_password_label: "Nouveau mot de passe".to_string(),
498 - reset_new_password_placeholder: "Entrez le nouveau mot de passe".to_string(),
499 - reset_password_requirements: "Le mot de passe doit contenir au moins 8 caractères".to_string(),
500 - reset_invalidate_sessions: "Invalider les sessions".to_string(),
501 - reset_invalidate_sessions_desc: "Déconnecte l'utilisateur de toutes ses sessions actives".to_string(),
502 - reset_button: "Réinitialiser".to_string(),
503 - delete_confirm_title: "Confirmer la suppression".to_string(),
504 - delete_confirm_message: "Êtes-vous sûr de vouloir supprimer cet utilisateur ?".to_string(),
505 - delete_confirm_username: "Nom d'utilisateur".to_string(),
506 - delete_confirm_email: "Email".to_string(),
507 - delete_irreversible: "Cette action est irréversible.".to_string(),
508 - delete_button: "Supprimer".to_string(),
509 - delete_error: "Erreur lors de la suppression".to_string(),
510 - network_error: "Erreur réseau".to_string(),
511 - cancel: "Annuler".to_string(),
512 - user_menu_profile: "Profil".to_string(),
513 - user_menu_logout: "Déconnexion".to_string(),
514 - user_menu_settings: "Paramètres".to_string(),
604 + table_username: i18n_service.translate("admin.users_list.table_username", locale_str).await,
605 + table_email: i18n_service.translate("admin.users_list.table_email", locale_str).await,
606 + table_roles: i18n_service.translate("admin.users_list.table_roles", locale_str).await,
607 + table_created_at: i18n_service.translate("admin.users_list.table_created_at", locale_str).await,
608 + table_last_login: i18n_service.translate("admin.users_list.table_last_login", locale_str).await,
609 + table_actions: i18n_service.translate("admin.users_list.table_actions", locale_str).await,
610 + never: i18n_service.translate("admin.users_list.never", locale_str).await,
611 + edit: i18n_service.translate("admin.users_list.edit", locale_str).await,
612 + delete: i18n_service.translate("admin.users_list.delete", locale_str).await,
613 + reset_password: i18n_service.translate("admin.users_list.reset_password", locale_str).await,
614 + no_users: i18n_service.translate("admin.users_list.no_users", locale_str).await,
615 + reset_password_title: i18n_service.translate("admin.users_list.reset_password_title", locale_str).await,
616 + reset_generate: i18n_service.translate("admin.users_list.reset_generate", locale_str).await,
617 + reset_custom: i18n_service.translate("admin.users_list.reset_custom", locale_str).await,
618 + reset_email: i18n_service.translate("admin.users_list.reset_email", locale_str).await,
619 + reset_new_password_label: i18n_service.translate("admin.users_list.reset_new_password_label", locale_str).await,
620 + reset_new_password_placeholder: i18n_service.translate("admin.users_list.reset_new_password_placeholder", locale_str).await,
621 + reset_password_requirements: i18n_service.translate("admin.users_list.reset_password_requirements", locale_str).await,
622 + reset_invalidate_sessions: i18n_service.translate("admin.users_list.reset_invalidate_sessions", locale_str).await,
623 + reset_invalidate_sessions_desc: i18n_service.translate("admin.users_list.reset_invalidate_sessions_desc", locale_str).await,
624 + reset_button: i18n_service.translate("admin.users_list.reset_button", locale_str).await,
625 + delete_confirm_title: i18n_service.translate("admin.users_list.delete_confirm_title", locale_str).await,
626 + delete_confirm_message: i18n_service.translate("admin.users_list.delete_confirm_message", locale_str).await,
627 + delete_confirm_username: i18n_service.translate("admin.users_list.delete_confirm_username", locale_str).await,
628 + delete_confirm_email: i18n_service.translate("admin.users_list.delete_confirm_email", locale_str).await,
629 + delete_irreversible: i18n_service.translate("admin.users_list.delete_irreversible", locale_str).await,
630 + delete_button: i18n_service.translate("admin.users_list.delete_button", locale_str).await,
631 + delete_error: i18n_service.translate("admin.users_list.delete_error", locale_str).await,
632 + network_error: i18n_service.translate("admin.users_list.network_error", locale_str).await,
633 + cancel: i18n_service.translate("common.cancel", locale_str).await,
634 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
635 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
636 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
637 + resetting: i18n_service.translate("admin.users_list.resetting", locale_str).await,
638 + reset_by_email_title: i18n_service.translate("admin.users_list.reset_by_email_title", locale_str).await,
639 + close: i18n_service.translate("admin.users_list.close", locale_str).await,
640 + password_reset_success_title: i18n_service.translate("admin.users_list.password_reset_success_title", locale_str).await,
641 + password_warning_once: i18n_service.translate("admin.users_list.password_warning_once", locale_str).await,
642 + new_password_label: i18n_service.translate("admin.users_list.new_password_label", locale_str).await,
643 + copy: i18n_service.translate("admin.users_list.copy", locale_str).await,
644 + password_copied: i18n_service.translate("admin.users_list.password_copied", locale_str).await,
645 + password_set_success: i18n_service.translate("admin.users_list.password_set_success", locale_str).await,
646 + reset_error: i18n_service.translate("admin.users_list.reset_error", locale_str).await,
515 647 };
516 648
517 649 Ok(template)
@@ -520,11 +652,15 @@ pub async fn list_users(
520 652 /// Handler for GET /admin/users/create - User creation form (PROTECTED - Admin only)
521 653 pub async fn create_user_form(
522 654 Extension(claims): Extension<Claims>,
655 + Extension(i18n_service): Extension<Arc<I18nService>>,
656 + Extension(locale): Extension<Locale>,
523 657 State(db): State<DatabaseConnection>,
524 658 session: Session,
525 659 ) -> Result<impl IntoResponse, AppError> {
660 + let locale_str = locale.as_str();
526 661 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
527 662 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
663 + let (app_domain, app_description) = load_app_domain_and_description(&db).await?;
528 664
529 665 use crate::services::role_service::RoleService;
530 666 let all_roles = RoleService::list_all_roles(&db).await?;
@@ -539,12 +675,12 @@ pub async fn create_user_form(
539 675 .collect();
540 676
541 677 let template = UserFormTemplate {
542 - page_title: "Créer un utilisateur".to_string(),
678 + page_title: i18n_service.translate("admin.users_list.create_user", locale_str).await,
543 679 csrf_token,
544 680 app_name: base_context.app_name,
545 681 app_version: base_context.app_version,
546 - app_domain: "localhost".to_string(),
547 - app_description: "Secure password manager".to_string(),
682 + app_domain,
683 + app_description,
548 684 username: base_context.username,
549 685 email: base_context.email,
550 686 initials: base_context.initials,
@@ -557,9 +693,30 @@ pub async fn create_user_form(
557 693 form_roles: Vec::new(),
558 694 available_roles: available_roles_display,
559 695 must_change_password: false,
560 - user_menu_profile: "Profil".to_string(),
561 - user_menu_logout: "Déconnexion".to_string(),
562 - user_menu_settings: "Paramètres".to_string(),
696 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
697 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
698 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
699 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
700 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
701 + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await,
702 + breadcrumb_mode: i18n_service.translate("admin.user_form.breadcrumb_create", locale_str).await,
703 + form_title: i18n_service.translate("admin.user_form.title_create", locale_str).await,
704 + back_to_list: i18n_service.translate("admin.user_form.back_to_list", locale_str).await,
705 + username_label: i18n_service.translate("admin.user_form.username_label", locale_str).await,
706 + username_placeholder: i18n_service.translate("admin.user_form.username_placeholder", locale_str).await,
707 + username_readonly_hint: i18n_service.translate("admin.user_form.username_readonly_hint", locale_str).await,
708 + email_label: i18n_service.translate("admin.user_form.email_label", locale_str).await,
709 + email_placeholder: i18n_service.translate("admin.user_form.email_placeholder", locale_str).await,
710 + password_label: i18n_service.translate("admin.user_form.password_label", locale_str).await,
711 + password_optional: i18n_service.translate("admin.user_form.password_optional", locale_str).await,
712 + password_placeholder: i18n_service.translate("admin.user_form.password_placeholder", locale_str).await,
713 + password_hint: i18n_service.translate("admin.user_form.password_hint", locale_str).await,
714 + roles_label: i18n_service.translate("admin.user_form.roles_label", locale_str).await,
715 + roles_hint: i18n_service.translate("admin.user_form.roles_hint", locale_str).await,
716 + must_change_password_label: i18n_service.translate("admin.user_form.must_change_password_label", locale_str).await,
717 + must_change_password_desc: i18n_service.translate("admin.user_form.must_change_password_desc", locale_str).await,
718 + cancel: i18n_service.translate("admin.user_form.cancel", locale_str).await,
719 + submit_label: i18n_service.translate("admin.user_form.submit_create", locale_str).await,
563 720 };
564 721
565 722 Ok(template)
@@ -628,12 +785,16 @@ pub async fn create_user(
628 785 /// Handler for GET /admin/users/:id - Edit user form (PROTECTED - Admin only)
629 786 pub async fn edit_user_form(
630 787 Extension(claims): Extension<Claims>,
788 + Extension(i18n_service): Extension<Arc<I18nService>>,
789 + Extension(locale): Extension<Locale>,
631 790 State(db): State<DatabaseConnection>,
632 791 session: Session,
633 792 axum::extract::Path(user_id): axum::extract::Path<uuid::Uuid>,
634 793 ) -> Result<impl IntoResponse, AppError> {
794 + let locale_str = locale.as_str();
635 795 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
636 796 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
797 + let (app_domain, app_description) = load_app_domain_and_description(&db).await?;
637 798
638 799 use crate::services::user_service::UserService;
639 800 let user = UserService::get_user_by_id(&db, user_id)
@@ -664,13 +825,17 @@ pub async fn edit_user_form(
664 825 })
665 826 .collect();
666 827
828 + let edit_title = i18n_service
829 + .translate("admin.users_list.edit_user_title", locale_str)
830 + .await;
831 +
667 832 let template = UserFormTemplate {
668 - page_title: format!("Modifier l'utilisateur {}", user.username),
833 + page_title: format!("{} {}", edit_title, user.username),
669 834 csrf_token,
670 835 app_name: base_context.app_name,
671 836 app_version: base_context.app_version,
672 - app_domain: "localhost".to_string(),
673 - app_description: "Secure password manager".to_string(),
837 + app_domain,
838 + app_description,
674 839 username: base_context.username,
675 840 email: base_context.email,
676 841 initials: base_context.initials,
@@ -683,9 +848,30 @@ pub async fn edit_user_form(
683 848 form_roles: current_role_names,
684 849 available_roles: available_roles_display,
685 850 must_change_password: user.must_change_password,
686 - user_menu_profile: "Profil".to_string(),
687 - user_menu_logout: "Déconnexion".to_string(),
688 - user_menu_settings: "Paramètres".to_string(),
851 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
852 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
853 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
854 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
855 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
856 + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await,
857 + breadcrumb_mode: i18n_service.translate("admin.user_form.breadcrumb_edit", locale_str).await,
858 + form_title: i18n_service.translate("admin.user_form.title_edit", locale_str).await,
859 + back_to_list: i18n_service.translate("admin.user_form.back_to_list", locale_str).await,
860 + username_label: i18n_service.translate("admin.user_form.username_label", locale_str).await,
861 + username_placeholder: i18n_service.translate("admin.user_form.username_placeholder", locale_str).await,
862 + username_readonly_hint: i18n_service.translate("admin.user_form.username_readonly_hint", locale_str).await,
863 + email_label: i18n_service.translate("admin.user_form.email_label", locale_str).await,
864 + email_placeholder: i18n_service.translate("admin.user_form.email_placeholder", locale_str).await,
865 + password_label: i18n_service.translate("admin.user_form.password_label", locale_str).await,
866 + password_optional: i18n_service.translate("admin.user_form.password_optional", locale_str).await,
867 + password_placeholder: i18n_service.translate("admin.user_form.password_placeholder", locale_str).await,
868 + password_hint: i18n_service.translate("admin.user_form.password_hint", locale_str).await,
869 + roles_label: i18n_service.translate("admin.user_form.roles_label", locale_str).await,
870 + roles_hint: i18n_service.translate("admin.user_form.roles_hint", locale_str).await,
871 + must_change_password_label: i18n_service.translate("admin.user_form.must_change_password_label", locale_str).await,
872 + must_change_password_desc: i18n_service.translate("admin.user_form.must_change_password_desc", locale_str).await,
873 + cancel: i18n_service.translate("admin.user_form.cancel", locale_str).await,
874 + submit_label: i18n_service.translate("admin.user_form.submit_edit", locale_str).await,
689 875 };
690 876
691 877 Ok(template)
@@ -883,7 +1069,7 @@ pub async fn reset_user_password(
883 1069 }
884 1070
885 1071 // ============================================================================
886 -// Admin Settings - Simplified for Rustwarden (no AppSettingsService)
1072 +// Admin Settings
887 1073 // ============================================================================
888 1074
889 1075 /// Admin application settings HTML template
@@ -906,7 +1092,7 @@ struct AdminSettingsTemplate {
906 1092 user_menu_profile: String,
907 1093 user_menu_logout: String,
908 1094 user_menu_settings: String,
909 - // Translated strings (hardcoded FR)
1095 + // Translated strings
910 1096 title: String,
911 1097 subtitle: String,
912 1098 general: String,
@@ -926,6 +1112,37 @@ struct AdminSettingsTemplate {
926 1112 breadcrumb_home: String,
927 1113 breadcrumb_admin: String,
928 1114 breadcrumb_settings: String,
1115 + // Password security section
1116 + password_security: String,
1117 + password_min_length: String,
1118 + password_min_length_hint: String,
1119 + password_complexity: String,
1120 + require_uppercase: String,
1121 + require_lowercase: String,
1122 + require_digits: String,
1123 + require_special: String,
1124 + password_expiration: String,
1125 + password_expiration_desc: String,
1126 + expiration_days: String,
1127 + expiration_alert: String,
1128 + expiration_alert_days: String,
1129 + // Audit section
1130 + audit_logging: String,
1131 + log_level: String,
1132 + log_level_debug: String,
1133 + log_level_info: String,
1134 + log_level_warn: String,
1135 + log_level_error: String,
1136 + log_level_hint: String,
1137 + log_retention: String,
1138 + log_retention_hint: String,
1139 + audit_info_message: String,
1140 + // Save section
1141 + save_changes: String,
1142 + save_changes_desc: String,
1143 + saving: String,
1144 + save_success: String,
1145 + save_error_prefix: String,
929 1146 }
930 1147
931 1148 impl_template_response!(AdminSettingsTemplate);
@@ -954,67 +1171,110 @@ struct AppSettingsTemplateContext {
954 1171 /// GET /admin/settings - Application settings page
955 1172 pub async fn get_app_settings(
956 1173 Extension(claims): Extension<crate::auth::jwt::Claims>,
1174 + Extension(i18n_service): Extension<Arc<I18nService>>,
1175 + Extension(locale): Extension<Locale>,
957 1176 State(db): State<DatabaseConnection>,
958 1177 session: Session,
959 1178 ) -> Result<impl IntoResponse, AppError> {
1179 + let locale_str = locale.as_str();
960 1180 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
961 1181 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
962 1182
963 - // Default settings (no AppSettingsService in Rustwarden)
1183 + // Load all settings from DB in one query for efficiency
1184 + let all_settings = AppSettingsService::get_all_settings(&db).await?;
1185 + let get_val = |key: &str, default: &str| -> String {
1186 + all_settings
1187 + .iter()
1188 + .find(|s| s.key == key)
1189 + .map(|s| s.value.clone())
1190 + .unwrap_or_else(|| default.to_string())
1191 + };
1192 +
1193 + let app_domain = get_val("app_domain", "localhost");
1194 + let app_description = get_val("app_description", "Secure password manager");
1195 +
964 1196 let settings_ctx = AppSettingsTemplateContext {
965 - app_name: "RustWarden".to_string(),
966 - app_domain: "localhost".to_string(),
967 - app_description: "Secure password manager".to_string(),
968 - allow_registration: "true".to_string(),
969 - validation_email_requise: "false".to_string(),
970 - password_min_length: "8".to_string(),
971 - password_require_uppercase: "true".to_string(),
972 - password_require_lowercase: "true".to_string(),
973 - password_require_digits: "true".to_string(),
974 - password_require_special: "false".to_string(),
975 - password_expiration_enabled: "false".to_string(),
976 - password_expiration_days: "90".to_string(),
977 - password_expiration_alert_enabled: "false".to_string(),
978 - password_expiration_alert_days_before: "7".to_string(),
979 - audit_log_level: "INFO".to_string(),
980 - audit_log_retention_days: "365".to_string(),
1197 + app_name: base_context.app_name.clone(),
1198 + app_domain: app_domain.clone(),
1199 + app_description: app_description.clone(),
1200 + allow_registration: get_val("allow_registration", "true"),
1201 + validation_email_requise: get_val("validation_email_required", "false"),
1202 + password_min_length: get_val("password_min_length", "8"),
1203 + password_require_uppercase: get_val("password_require_uppercase", "true"),
1204 + password_require_lowercase: get_val("password_require_lowercase", "true"),
1205 + password_require_digits: get_val("password_require_digits", "true"),
1206 + password_require_special: get_val("password_require_special", "false"),
1207 + password_expiration_enabled: get_val("password_expiration_enabled", "false"),
1208 + password_expiration_days: get_val("password_expiration_days", "90"),
1209 + password_expiration_alert_enabled: get_val("password_expiration_alert_enabled", "false"),
1210 + password_expiration_alert_days_before: get_val("password_expiration_alert_days_before", "7"),
1211 + audit_log_level: get_val("audit_log_level", "INFO"),
1212 + audit_log_retention_days: get_val("audit_log_retention_days", "365"),
981 1213 };
982 1214
983 1215 let template = AdminSettingsTemplate {
984 - page_title: "Paramètres de l'application".to_string(),
1216 + page_title: i18n_service.translate("admin.settings.title", locale_str).await,
985 1217 csrf_token,
986 1218 app_name: base_context.app_name,
987 1219 app_version: base_context.app_version,
988 - app_domain: "localhost".to_string(),
989 - app_description: "Secure password manager".to_string(),
1220 + app_domain,
1221 + app_description,
990 1222 username: base_context.username,
991 1223 email: base_context.email,
992 1224 initials: base_context.initials,
993 1225 avatar_color: base_context.avatar_color,
994 1226 roles_text: base_context.roles.join(", "),
995 1227 settings: settings_ctx,
996 - title: "Paramètres de l'application".to_string(),
997 - subtitle: "Configurez les paramètres généraux de l'application".to_string(),
998 - general: "Général".to_string(),
999 - app_name_label: "Nom de l'application".to_string(),
1000 - app_domain_label: "Domaine".to_string(),
1001 - app_domain_placeholder: "example.com".to_string(),
1002 - app_description_label: "Description".to_string(),
1003 - allow_registration_label: "Autoriser l'inscription".to_string(),
1004 - allow_registration_desc: "Permettre aux nouveaux utilisateurs de s'inscrire".to_string(),
1005 - email_validation_required_label: "Validation email requise".to_string(),
1006 - email_validation_required_desc: "Exiger la validation de l'email lors de l'inscription".to_string(),
1007 - save_button: "Enregistrer".to_string(),
1008 - cancel: "Annuler".to_string(),
1009 - saved: "Paramètres enregistrés avec succès".to_string(),
1010 - error: "Erreur".to_string(),
1011 - network_error: "Erreur réseau".to_string(),
1012 - breadcrumb_home: "Accueil".to_string(),
1013 - breadcrumb_admin: "Administration".to_string(),
1014 - breadcrumb_settings: "Paramètres".to_string(),
1015 - user_menu_profile: "Profil".to_string(),
1016 - user_menu_logout: "Déconnexion".to_string(),
1017 - user_menu_settings: "Paramètres".to_string(),
1228 + title: i18n_service.translate("admin.settings.title", locale_str).await,
1229 + subtitle: i18n_service.translate("admin.settings.subtitle", locale_str).await,
1230 + general: i18n_service.translate("admin.settings.general", locale_str).await,
1231 + app_name_label: i18n_service.translate("admin.settings.app_name", locale_str).await,
1232 + app_domain_label: i18n_service.translate("admin.settings.app_domain", locale_str).await,
1233 + app_domain_placeholder: i18n_service.translate("admin.settings.app_domain_placeholder", locale_str).await,
1234 + app_description_label: i18n_service.translate("admin.settings.app_description", locale_str).await,
1235 + allow_registration_label: i18n_service.translate("admin.settings.allow_registration", locale_str).await,
1236 + allow_registration_desc: i18n_service.translate("admin.settings.allow_registration_desc", locale_str).await,
1237 + email_validation_required_label: i18n_service.translate("admin.settings.email_validation_required", locale_str).await,
1238 + email_validation_required_desc: i18n_service.translate("admin.settings.email_validation_required_desc", locale_str).await,
1239 + save_button: i18n_service.translate("admin.settings.save", locale_str).await,
1240 + cancel: i18n_service.translate("admin.settings.cancel", locale_str).await,
1241 + saved: i18n_service.translate("admin.settings.saved", locale_str).await,
1242 + error: i18n_service.translate("admin.settings.error", locale_str).await,
1243 + network_error: i18n_service.translate("admin.settings.network_error", locale_str).await,
1244 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1245 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
1246 + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await,
1247 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
1248 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1249 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1250 + password_security: i18n_service.translate("admin.settings.password_security", locale_str).await,
1251 + password_min_length: i18n_service.translate("admin.settings.password_min_length", locale_str).await,
1252 + password_min_length_hint: i18n_service.translate("admin.settings.password_min_length_hint", locale_str).await,
1253 + password_complexity: i18n_service.translate("admin.settings.password_complexity", locale_str).await,
1254 + require_uppercase: i18n_service.translate("admin.settings.require_uppercase", locale_str).await,
1255 + require_lowercase: i18n_service.translate("admin.settings.require_lowercase", locale_str).await,
1256 + require_digits: i18n_service.translate("admin.settings.require_digits", locale_str).await,
1257 + require_special: i18n_service.translate("admin.settings.require_special", locale_str).await,
1258 + password_expiration: i18n_service.translate("admin.settings.password_expiration", locale_str).await,
1259 + password_expiration_desc: i18n_service.translate("admin.settings.password_expiration_desc", locale_str).await,
1260 + expiration_days: i18n_service.translate("admin.settings.expiration_days", locale_str).await,
1261 + expiration_alert: i18n_service.translate("admin.settings.expiration_alert", locale_str).await,
1262 + expiration_alert_days: i18n_service.translate("admin.settings.expiration_alert_days", locale_str).await,
1263 + audit_logging: i18n_service.translate("admin.settings.audit_logging", locale_str).await,
1264 + log_level: i18n_service.translate("admin.settings.log_level", locale_str).await,
1265 + log_level_debug: i18n_service.translate("admin.settings.log_level_debug", locale_str).await,
1266 + log_level_info: i18n_service.translate("admin.settings.log_level_info", locale_str).await,
1267 + log_level_warn: i18n_service.translate("admin.settings.log_level_warn", locale_str).await,
1268 + log_level_error: i18n_service.translate("admin.settings.log_level_error", locale_str).await,
1269 + log_level_hint: i18n_service.translate("admin.settings.log_level_hint", locale_str).await,
1270 + log_retention: i18n_service.translate("admin.settings.log_retention", locale_str).await,
1271 + log_retention_hint: i18n_service.translate("admin.settings.log_retention_hint", locale_str).await,
1272 + audit_info_message: i18n_service.translate("admin.settings.audit_info_message", locale_str).await,
1273 + save_changes: i18n_service.translate("admin.settings.save_changes", locale_str).await,
1274 + save_changes_desc: i18n_service.translate("admin.settings.save_changes_desc", locale_str).await,
1275 + saving: i18n_service.translate("admin.settings.saving", locale_str).await,
1276 + save_success: i18n_service.translate("admin.settings.save_success", locale_str).await,
1277 + save_error_prefix: i18n_service.translate("admin.settings.save_error_prefix", locale_str).await,
1018 1278 };
1019 1279
1020 1280 Ok(template)
@@ -1065,6 +1325,29 @@ struct AdminErrorsTemplate {
1065 1325 avatar_color: String,
1066 1326 roles_text: String,
1067 1327 error_codes: Vec<ErrorCodeInfo>,
1328 + // User menu
1329 + user_menu_profile: String,
1330 + user_menu_logout: String,
1331 + user_menu_settings: String,
1332 + // Translated strings
1333 + breadcrumb_home: String,
1334 + breadcrumb_admin: String,
1335 + breadcrumb_errors: String,
1336 + title: String,
1337 + description: String,
1338 + table_code: String,
1339 + table_description: String,
1340 + table_category: String,
1341 + table_middleware: String,
1342 + table_context: String,
1343 + table_example: String,
1344 + legend_title: String,
1345 + legend_code: String,
1346 + legend_description: String,
1347 + legend_category: String,
1348 + legend_middleware: String,
1349 + legend_context: String,
1350 + legend_example: String,
1068 1351 }
1069 1352
1070 1353 impl_template_response!(AdminErrorsTemplate);
@@ -1072,125 +1355,88 @@ impl_template_response!(AdminErrorsTemplate);
1072 1355 /// Handler for GET /admin/errors - Error codes documentation page
1073 1356 pub async fn admin_errors(
1074 1357 Extension(claims): Extension<Claims>,
1358 + Extension(i18n_service): Extension<Arc<I18nService>>,
1359 + Extension(locale): Extension<Locale>,
1075 1360 session: Session,
1076 1361 State(db): State<DatabaseConnection>,
1077 1362 ) -> Result<impl IntoResponse, AppError> {
1363 + let locale_str = locale.as_str();
1078 1364 let base_context = build_base_context_with_claims(&session, &db, &claims).await?;
1079 1365 let csrf_token = csrf::get_or_create_csrf_token(&session).await?;
1366 + let (app_domain, app_description) = load_app_domain_and_description(&db).await?;
1080 1367
1081 1368 use crate::api::middleware::error_handler::ErrorCode;
1082 - let error_codes = vec![
1083 - ErrorCodeInfo {
1084 - code: ErrorCode::Auth001.as_str().to_string(),
1085 - description: "Identifiants invalides".to_string(),
1086 - category: "Authentification".to_string(),
1087 - middleware: "auth_required".to_string(),
1088 - context: "L'utilisateur a fourni des identifiants incorrects lors de la connexion".to_string(),
1089 - example: "Tentative de connexion avec un mot de passe incorrect".to_string(),
1090 - },
1091 - ErrorCodeInfo {
1092 - code: ErrorCode::Auth002.as_str().to_string(),
1093 - description: "Token invalide".to_string(),
1094 - category: "Authentification".to_string(),
1095 - middleware: "auth_required".to_string(),
1096 - context: "Le token JWT fourni est invalide ou malformé".to_string(),
1097 - example: "Token JWT corrompu ou signé avec une clé différente".to_string(),
1098 - },
1099 - ErrorCodeInfo {
1100 - code: ErrorCode::Auth003.as_str().to_string(),
1101 - description: "Token expiré".to_string(),
1102 - category: "Authentification".to_string(),
1103 - middleware: "auth_required".to_string(),
1104 - context: "Le token JWT a expiré et doit être renouvelé".to_string(),
1105 - example: "Tentative d'accès avec un token expiré après la durée de validité".to_string(),
1106 - },
1107 - ErrorCodeInfo {
1108 - code: ErrorCode::Csrf001.as_str().to_string(),
1109 - description: "Token CSRF manquant".to_string(),
1110 - category: "CSRF Protection".to_string(),
1111 - middleware: "csrf".to_string(),
1112 - context: "La requête POST/PUT/DELETE ne contient pas de token CSRF".to_string(),
1113 - example: "Formulaire soumis sans token CSRF dans le header ou le body".to_string(),
1114 - },
1115 - ErrorCodeInfo {
1116 - code: ErrorCode::Csrf002.as_str().to_string(),
1117 - description: "Token CSRF invalide".to_string(),
1118 - category: "CSRF Protection".to_string(),
1119 - middleware: "csrf".to_string(),
1120 - context: "Le token CSRF fourni ne correspond pas au token de la session".to_string(),
1121 - example: "Token CSRF modifié ou volé".to_string(),
1122 - },
1123 - ErrorCodeInfo {
1124 - code: ErrorCode::Rate001.as_str().to_string(),
1125 - description: "Limite de taux dépassée".to_string(),
1126 - category: "Rate Limiting".to_string(),
1127 - middleware: "rate_limit".to_string(),
1128 - context: "Le nombre de requêtes par minute a été dépassé".to_string(),
1129 - example: "Plus de 100 requêtes/min sur les endpoints admin".to_string(),
1130 - },
1131 - ErrorCodeInfo {
1132 - code: ErrorCode::Valid001.as_str().to_string(),
1133 - description: "Données d'entrée invalides".to_string(),
1134 - category: "Validation".to_string(),
1135 - middleware: "validation".to_string(),
1136 - context: "Les données fournies ne respectent pas les règles de validation".to_string(),
1137 - example: "Email au format invalide ou champ requis manquant".to_string(),
1138 - },
1139 - ErrorCodeInfo {
1140 - code: ErrorCode::Valid002.as_str().to_string(),
1141 - description: "Complexité du mot de passe non respectée".to_string(),
1142 - category: "Validation".to_string(),
1143 - middleware: "password_complexity".to_string(),
1144 - context: "Le mot de passe ne respecte pas les règles de complexité configurées".to_string(),
1145 - example: "Mot de passe trop court ou ne contenant pas les caractères requis".to_string(),
1146 - },
1147 - ErrorCodeInfo {
1148 - code: ErrorCode::Rbac001.as_str().to_string(),
1149 - description: "Permissions insuffisantes".to_string(),
1150 - category: "Autorisation".to_string(),
1151 - middleware: "admin_required".to_string(),
1152 - context: "L'utilisateur n'a pas les permissions nécessaires pour cette action".to_string(),
1153 - example: "Utilisateur non-admin tentant d'accéder à /admin/users".to_string(),
1154 - },
1155 - ErrorCodeInfo {
1156 - code: ErrorCode::Rbac002.as_str().to_string(),
1157 - description: "Rôle administrateur requis".to_string(),
1158 - category: "Autorisation".to_string(),
1159 - middleware: "admin_required".to_string(),
1160 - context: "L'action nécessite le rôle administrateur".to_string(),
1161 - example: "Tentative de suppression d'utilisateur sans rôle admin".to_string(),
1162 - },
1163 - ErrorCodeInfo {
1164 - code: ErrorCode::Db001.as_str().to_string(),
1165 - description: "Erreur d'opération de base de données".to_string(),
1166 - category: "Base de données".to_string(),
1167 - middleware: "database".to_string(),
1168 - context: "Une opération de base de données a échoué".to_string(),
1169 - example: "Contrainte de clé étrangère violée ou connexion DB perdue".to_string(),
1170 - },
1171 - ErrorCodeInfo {
1172 - code: ErrorCode::Db002.as_str().to_string(),
1173 - description: "Ressource non trouvée".to_string(),
1174 - category: "Base de données".to_string(),
1175 - middleware: "database".to_string(),
1176 - context: "La ressource demandée n'existe pas dans la base de données".to_string(),
1177 - example: "Utilisateur avec ID inexistant".to_string(),
1178 - },
1369 +
1370 + struct ErrorDef {
1371 + code_enum: ErrorCode,
1372 + desc_key: &'static str,
1373 + cat_key: &'static str,
1374 + middleware: &'static str,
1375 + ctx_key: &'static str,
1376 + ex_key: &'static str,
1377 + }
1378 +
1379 + let error_defs = vec![
1380 + ErrorDef { code_enum: ErrorCode::Auth001, desc_key: "admin.errors.error_auth001_desc", cat_key: "admin.errors.error_auth001_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth001_context", ex_key: "admin.errors.error_auth001_example" },
1381 + ErrorDef { code_enum: ErrorCode::Auth002, desc_key: "admin.errors.error_auth002_desc", cat_key: "admin.errors.error_auth002_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth002_context", ex_key: "admin.errors.error_auth002_example" },
1382 + ErrorDef { code_enum: ErrorCode::Auth003, desc_key: "admin.errors.error_auth003_desc", cat_key: "admin.errors.error_auth003_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth003_context", ex_key: "admin.errors.error_auth003_example" },
1383 + ErrorDef { code_enum: ErrorCode::Csrf001, desc_key: "admin.errors.error_csrf001_desc", cat_key: "admin.errors.error_csrf001_category", middleware: "csrf", ctx_key: "admin.errors.error_csrf001_context", ex_key: "admin.errors.error_csrf001_example" },
1384 + ErrorDef { code_enum: ErrorCode::Csrf002, desc_key: "admin.errors.error_csrf002_desc", cat_key: "admin.errors.error_csrf002_category", middleware: "csrf", ctx_key: "admin.errors.error_csrf002_context", ex_key: "admin.errors.error_csrf002_example" },
1385 + ErrorDef { code_enum: ErrorCode::Rate001, desc_key: "admin.errors.error_rate001_desc", cat_key: "admin.errors.error_rate001_category", middleware: "rate_limit", ctx_key: "admin.errors.error_rate001_context", ex_key: "admin.errors.error_rate001_example" },
1386 + ErrorDef { code_enum: ErrorCode::Valid001, desc_key: "admin.errors.error_valid001_desc", cat_key: "admin.errors.error_valid001_category", middleware: "validation", ctx_key: "admin.errors.error_valid001_context", ex_key: "admin.errors.error_valid001_example" },
1387 + ErrorDef { code_enum: ErrorCode::Valid002, desc_key: "admin.errors.error_valid002_desc", cat_key: "admin.errors.error_valid002_category", middleware: "password_complexity", ctx_key: "admin.errors.error_valid002_context", ex_key: "admin.errors.error_valid002_example" },
1388 + ErrorDef { code_enum: ErrorCode::Rbac001, desc_key: "admin.errors.error_rbac001_desc", cat_key: "admin.errors.error_rbac001_category", middleware: "admin_required", ctx_key: "admin.errors.error_rbac001_context", ex_key: "admin.errors.error_rbac001_example" },
1389 + ErrorDef { code_enum: ErrorCode::Rbac002, desc_key: "admin.errors.error_rbac002_desc", cat_key: "admin.errors.error_rbac002_category", middleware: "admin_required", ctx_key: "admin.errors.error_rbac002_context", ex_key: "admin.errors.error_rbac002_example" },
1390 + ErrorDef { code_enum: ErrorCode::Db001, desc_key: "admin.errors.error_db001_desc", cat_key: "admin.errors.error_db001_category", middleware: "database", ctx_key: "admin.errors.error_db001_context", ex_key: "admin.errors.error_db001_example" },
1391 + ErrorDef { code_enum: ErrorCode::Db002, desc_key: "admin.errors.error_db002_desc", cat_key: "admin.errors.error_db002_category", middleware: "database", ctx_key: "admin.errors.error_db002_context", ex_key: "admin.errors.error_db002_example" },
1179 1392 ];
1180 1393
1394 + let mut error_codes = Vec::new();
1395 + for def in &error_defs {
1396 + error_codes.push(ErrorCodeInfo {
1397 + code: def.code_enum.as_str().to_string(),
1398 + description: i18n_service.translate(def.desc_key, locale_str).await,
1399 + category: i18n_service.translate(def.cat_key, locale_str).await,
1400 + middleware: def.middleware.to_string(),
1401 + context: i18n_service.translate(def.ctx_key, locale_str).await,
1402 + example: i18n_service.translate(def.ex_key, locale_str).await,
1403 + });
1404 + }
1405 +
1181 1406 let template = AdminErrorsTemplate {
1182 - page_title: "Codes d'Erreur".to_string(),
1407 + page_title: i18n_service.translate("admin.errors.title", locale_str).await,
1183 1408 csrf_token,
1184 1409 app_name: base_context.app_name,
1185 1410 app_version: base_context.app_version,
1186 - app_domain: "localhost".to_string(),
1187 - app_description: "Secure password manager".to_string(),
1411 + app_domain,
1412 + app_description,
1188 1413 username: base_context.username,
1189 1414 email: base_context.email,
1190 1415 initials: base_context.initials,
1191 1416 avatar_color: base_context.avatar_color,
1192 1417 roles_text: base_context.roles.join(", "),
1193 1418 error_codes,
1419 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
1420 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1421 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1422 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1423 + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await,
1424 + breadcrumb_errors: i18n_service.translate("admin.errors.breadcrumb_errors", locale_str).await,
1425 + title: i18n_service.translate("admin.errors.title", locale_str).await,
1426 + description: i18n_service.translate("admin.errors.description", locale_str).await,
1427 + table_code: i18n_service.translate("admin.errors.table_code", locale_str).await,
1428 + table_description: i18n_service.translate("admin.errors.table_description", locale_str).await,
1429 + table_category: i18n_service.translate("admin.errors.table_category", locale_str).await,
1430 + table_middleware: i18n_service.translate("admin.errors.table_middleware", locale_str).await,
1431 + table_context: i18n_service.translate("admin.errors.table_context", locale_str).await,
1432 + table_example: i18n_service.translate("admin.errors.table_example", locale_str).await,
1433 + legend_title: i18n_service.translate("admin.errors.legend_title", locale_str).await,
1434 + legend_code: i18n_service.translate("admin.errors.legend_code", locale_str).await,
1435 + legend_description: i18n_service.translate("admin.errors.legend_description", locale_str).await,
1436 + legend_category: i18n_service.translate("admin.errors.legend_category", locale_str).await,
1437 + legend_middleware: i18n_service.translate("admin.errors.legend_middleware", locale_str).await,
1438 + legend_context: i18n_service.translate("admin.errors.legend_context", locale_str).await,
1439 + legend_example: i18n_service.translate("admin.errors.legend_example", locale_str).await,
1194 1440 };
1195 1441
1196 1442 Ok(template)
M src/api/handlers/home_handler.rs
+177 -139
@@ -8,91 +8,18 @@ use axum::{
8 8 response::IntoResponse,
9 9 };
10 10 use sea_orm::DatabaseConnection;
11 +use std::collections::HashMap;
12 +use std::sync::Arc;
11 13
14 +use crate::api::dto::home_dto::{AccountStatDTO, UpcomingFeatureDTO};
15 +use crate::api::dto::navigation_dto::NavigationDTO;
16 +use crate::api::dto::user_profile_dto::UserProfileDTO;
12 17 use crate::auth::jwt::Claims;
13 18 use crate::error::AppError;
19 +use crate::i18n::extractor::Locale;
20 +use crate::i18n::service::I18nService;
14 21 use crate::services::user_service::UserService;
15 22
16 -/// Account stat for home page
17 -#[derive(Debug, Clone)]
18 -struct AccountStatDTO {
19 - label: String,
20 - value: String,
21 - icon: String,
22 -}
23 -
24 -/// Upcoming feature for home page
25 -#[derive(Debug, Clone)]
26 -struct UpcomingFeatureDTO {
27 - id: String,
28 - name: String,
29 - description: String,
30 - icon: String,
31 - badge: String,
32 -}
33 -
34 -/// Simplified navigation item for templates
35 -#[derive(Clone)]
36 -#[allow(dead_code)]
37 -struct NavItem {
38 - id: String,
39 - label: String,
40 - icon: String,
41 - url: String,
42 - active: bool,
43 - enabled: bool,
44 - badge: String,
45 - has_badge: bool,
46 - disabled_message: String,
47 - has_disabled_message: bool,
48 -}
49 -
50 -fn build_nav_items(roles: &[String], active_id: &str) -> Vec<NavItem> {
51 - let mut items = vec![
52 - NavItem {
53 - id: "home".to_string(),
54 - label: "Accueil".to_string(),
55 - icon: "home".to_string(),
56 - url: "/home".to_string(),
57 - active: active_id == "home",
58 - enabled: true,
59 - badge: String::new(),
60 - has_badge: false,
61 - disabled_message: String::new(),
62 - has_disabled_message: false,
63 - },
64 - NavItem {
65 - id: "settings".to_string(),
66 - label: "Paramètres".to_string(),
67 - icon: "settings".to_string(),
68 - url: "/settings".to_string(),
69 - active: active_id == "settings",
70 - enabled: true,
71 - badge: String::new(),
72 - has_badge: false,
73 - disabled_message: String::new(),
74 - has_disabled_message: false,
75 - },
76 - ];
77 -
78 - if roles.contains(&"admin".to_string()) {
79 - items.push(NavItem {
80 - id: "admin".to_string(),
81 - label: "Administration".to_string(),
82 - icon: "shield".to_string(),
83 - url: "/admin".to_string(),
84 - active: active_id == "admin",
85 - enabled: true,
86 - badge: String::new(),
87 - has_badge: false,
88 - disabled_message: String::new(),
89 - has_disabled_message: false,
90 - });
91 - }
92 -
93 - items
94 -}
95 -
96 23 /// Home page template
97 24 #[derive(Template)]
98 25 #[template(path = "home.html")]
@@ -112,6 +39,11 @@ struct HomeTemplate {
112 39 nav_items: Vec<NavItem>,
113 40 has_pending_password_change: bool,
114 41 welcome_message: String,
42 + home_subtitle: String,
43 + home_my_account: String,
44 + home_upcoming_features: String,
45 + home_did_you_know: String,
46 + home_did_you_know_text: String,
115 47 account_stats: Vec<AccountStatDTO>,
116 48 upcoming_features: Vec<UpcomingFeatureDTO>,
117 49 user_menu_profile: String,
@@ -119,11 +51,30 @@ struct HomeTemplate {
119 51 user_menu_settings: String,
120 52 }
121 53
54 +/// Simplified navigation item for templates
55 +#[derive(Clone)]
56 +#[allow(dead_code)]
57 +struct NavItem {
58 + #[allow(dead_code)]
59 + id: String,
60 + label: String,
61 + icon: String,
62 + url: String,
63 + active: bool,
64 + enabled: bool,
65 + badge: String,
66 + has_badge: bool,
67 + disabled_message: String,
68 + has_disabled_message: bool,
69 +}
70 +
122 71 crate::impl_template_response!(HomeTemplate);
123 72
124 73 /// Handler for GET /home - Home page (PROTECTED)
125 74 pub async fn home(
126 75 Extension(claims): Extension<Claims>,
76 + Extension(i18n_service): Extension<Arc<I18nService>>,
77 + Extension(locale): Extension<Locale>,
127 78 State(db): State<DatabaseConnection>,
128 79 ) -> Result<impl IntoResponse, AppError> {
129 80 let username = claims.username.clone();
@@ -137,86 +88,173 @@ pub async fn home(
137 88 })?;
138 89
139 90 let roles = UserService::get_user_roles(&db, user_model.id).await?;
91 + let user = UserProfileDTO::new(user_model, roles.clone());
140 92
141 - let initials = {
142 - let parts: Vec<&str> = user_model.username.split_whitespace().collect();
143 - match parts.len() {
144 - 0 => "??".to_string(),
145 - 1 => user_model.username.chars().take(2).collect::<String>().to_uppercase(),
146 - _ => format!(
147 - "{}{}",
148 - parts[0].chars().next().unwrap_or('?'),
149 - parts[1].chars().next().unwrap_or('?')
150 - ).to_uppercase(),
151 - }
152 - };
93 + let locale_str = locale.as_str();
153 94
154 - let avatar_color = {
155 - let hash: u32 = user_model.username
156 - .bytes()
157 - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
158 - let hue = hash % 180;
159 - let sat = 60 + (hash % 20) as u8;
160 - let light = 45 + (hash % 15) as u8;
161 - format!("hsl({}, {}%, {}%)", hue, sat, light)
162 - };
163 -
164 - let layout = crate::templates::LayoutContext::new("Accueil", "");
95 + // Translate welcome message with variable interpolation
96 + let mut vars = HashMap::new();
97 + vars.insert("username".to_string(), user.username.clone());
98 + let welcome_message = i18n_service
99 + .translate_with_vars("home.welcome", locale_str, &vars)
100 + .await;
165 101
166 - let nav_items = build_nav_items(&roles, "home");
102 + // Translate section titles
103 + let home_subtitle = i18n_service.translate("home.subtitle", locale_str).await;
104 + let home_my_account = i18n_service.translate("home.my_account", locale_str).await;
105 + let home_upcoming_features = i18n_service
106 + .translate("home.upcoming_features", locale_str)
107 + .await;
108 + let home_did_you_know = i18n_service
109 + .translate("home.did_you_know", locale_str)
110 + .await;
111 + let home_did_you_know_text = i18n_service
112 + .translate("home.did_you_know_text", locale_str)
113 + .await;
167 114
168 - let welcome_message = format!("Bienvenue, {} !", user_model.username);
115 + // Build translated account stats
116 + let stat_username_label = i18n_service
117 + .translate("home.stat_username", locale_str)
118 + .await;
119 + let stat_email_label = i18n_service.translate("home.stat_email", locale_str).await;
120 + let stat_member_since_label = i18n_service
121 + .translate("home.stat_member_since", locale_str)
122 + .await;
123 + let stat_roles_label = i18n_service.translate("home.stat_roles", locale_str).await;
169 124
170 125 let account_stats = vec![
171 - AccountStatDTO {
172 - label: "Rôles".to_string(),
173 - value: roles.len().to_string(),
174 - icon: "shield".to_string(),
175 - },
176 - AccountStatDTO {
177 - label: "Email vérifié".to_string(),
178 - value: if user_model.email_verified { "Oui" } else { "Non" }.to_string(),
179 - icon: "mail".to_string(),
180 - },
126 + AccountStatDTO::new(&stat_username_label, &user.username, "user"),
127 + AccountStatDTO::new(&stat_email_label, &user.email, "mail"),
128 + AccountStatDTO::new(
129 + &stat_member_since_label,
130 + &user.created_at.format("%d/%m/%Y").to_string(),
131 + "calendar",
132 + ),
133 + AccountStatDTO::new(&stat_roles_label, &user.roles.join(", "), "shield"),
181 134 ];
182 135
136 + // Build translated upcoming features
137 + let badge_soon = i18n_service.translate("home.badge_soon", locale_str).await;
138 + let badge_planned = i18n_service
139 + .translate("home.badge_planned", locale_str)
140 + .await;
141 +
183 142 let upcoming_features = vec![
184 - UpcomingFeatureDTO {
185 - id: "secrets".to_string(),
186 - name: "Gestion des secrets".to_string(),
187 - description: "Stockage sécurisé de mots de passe et secrets".to_string(),
188 - icon: "folder".to_string(),
189 - badge: "Bientôt".to_string(),
190 - },
191 - UpcomingFeatureDTO {
192 - id: "2fa".to_string(),
193 - name: "Authentification 2FA".to_string(),
194 - description: "Authentification à deux facteurs".to_string(),
195 - icon: "shield".to_string(),
196 - badge: "Planifié".to_string(),
197 - },
143 + UpcomingFeatureDTO::new(
144 + "005",
145 + &i18n_service
146 + .translate("home.feature_files_name", locale_str)
147 + .await,
148 + &i18n_service
149 + .translate("home.feature_files_desc", locale_str)
150 + .await,
151 + "folder",
152 + &badge_soon,
153 + ),
154 + UpcomingFeatureDTO::new(
155 + "006",
156 + &i18n_service
157 + .translate("home.feature_upload_name", locale_str)
158 + .await,
159 + &i18n_service
160 + .translate("home.feature_upload_desc", locale_str)
161 + .await,
162 + "upload",
163 + &badge_soon,
164 + ),
165 + UpcomingFeatureDTO::new(
166 + "007",
167 + &i18n_service
168 + .translate("home.feature_webdav_name", locale_str)
169 + .await,
170 + &i18n_service
171 + .translate("home.feature_webdav_desc", locale_str)
172 + .await,
173 + "cloud",
174 + &badge_planned,
175 + ),
176 + UpcomingFeatureDTO::new(
177 + "008",
178 + &i18n_service
179 + .translate("home.feature_mobile_name", locale_str)
180 + .await,
181 + &i18n_service
182 + .translate("home.feature_mobile_desc", locale_str)
183 + .await,
184 + "smartphone",
185 + &badge_planned,
186 + ),
198 187 ];
199 188
189 + // Translate user menu
190 + let user_menu_profile = i18n_service
191 + .translate("user_menu.profile", locale_str)
192 + .await;
193 + let user_menu_logout = i18n_service
194 + .translate("user_menu.logout", locale_str)
195 + .await;
196 + let user_menu_settings = i18n_service
197 + .translate("user_menu.settings", locale_str)
198 + .await;
199 +
200 + // Build navigation
201 + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
202 + let nav_items: Vec<NavItem> = navigation
203 + .items
204 + .iter()
205 + .enumerate()
206 + .map(|(i, item)| NavItem {
207 + id: item.id.clone(),
208 + label: item.label.clone(),
209 + icon: item.icon.clone(),
210 + url: item.url.clone(),
211 + active: i == 0,
212 + enabled: item.enabled,
213 + badge: item.badge.clone(),
214 + has_badge: item.has_badge,
215 + disabled_message: item.disabled_message.clone(),
216 + has_disabled_message: item.has_disabled_message,
217 + })
218 + .collect();
219 +
220 + // Load app settings from database
221 + use crate::services::app_settings_service::AppSettingsService;
222 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
223 + .await?
224 + .map(|s| s.value)
225 + .unwrap_or_else(|| "localhost".to_string());
226 + let app_description = AppSettingsService::get_setting(&db, "app_description")
227 + .await?
228 + .map(|s| s.value)
229 + .unwrap_or_else(|| "Secure password manager".to_string());
230 +
231 + let layout = crate::templates::LayoutContext::new("", "");
232 +
200 233 let template = HomeTemplate {
201 234 page_title: layout.page_title.clone(),
202 235 csrf_token: layout.csrf_token.clone(),
203 236 app_name: layout.app_name.clone(),
204 237 app_version: layout.app_version.clone(),
205 - app_domain: "localhost".to_string(),
206 - app_description: "Secure password manager".to_string(),
207 - username: user_model.username.clone(),
208 - email: user_model.email.clone(),
209 - initials,
210 - avatar_color,
211 - roles_text: roles.join(", "),
238 + app_domain,
239 + app_description,
240 + username: user.username.clone(),
241 + email: user.email.clone(),
242 + initials: user.initials.clone(),
243 + avatar_color: user.avatar_color.clone(),
244 + roles_text: user.roles.join(", "),
212 245 nav_items,
213 246 has_pending_password_change: false,
214 247 welcome_message,
248 + home_subtitle,
249 + home_my_account,
250 + home_upcoming_features,
251 + home_did_you_know,
252 + home_did_you_know_text,
215 253 account_stats,
216 254 upcoming_features,
217 - user_menu_profile: "Profil".to_string(),
218 - user_menu_logout: "Déconnexion".to_string(),
219 - user_menu_settings: "Paramètres".to_string(),
255 + user_menu_profile,
256 + user_menu_logout,
257 + user_menu_settings,
220 258 };
221 259
222 260 Ok(template)
M src/api/handlers/login_handler.rs
+320 -56
@@ -4,13 +4,15 @@
4 4 /// Handles the login page display (GET /login).
5 5 use askama::Template;
6 6 use axum::{
7 - extract::State,
7 + extract::{Extension, State},
8 8 http::HeaderMap,
9 9 response::IntoResponse,
10 10 };
11 11 use axum_extra::extract::CookieJar;
12 12 use sea_orm::DatabaseConnection;
13 +use std::sync::Arc;
13 14
15 +use crate::i18n::service::I18nService;
14 16 use crate::templates::LayoutContext;
15 17
16 18 /// Login page template
@@ -21,7 +23,6 @@ struct LoginTemplate {
21 23 app_version: String,
22 24 current_locale: String,
23 25 supported_languages: Vec<(String, String, bool)>,
24 - // Translated strings (hardcoded FR)
25 26 page_title: String,
26 27 page_description: String,
27 28 subtitle: String,
@@ -65,6 +66,17 @@ struct ResendVerificationTemplate {
65 66 app_version: String,
66 67 app_domain: String,
67 68 app_description: String,
69 + page_title: String,
70 + title: String,
71 + subtitle: String,
72 + email_label: String,
73 + email_placeholder: String,
74 + submit: String,
75 + back_to_login: String,
76 + error_default: String,
77 + success_default: String,
78 + error_missing_email: String,
79 + error_cannot_send: String,
68 80 }
69 81
70 82 /// Reset password page template
@@ -77,13 +89,23 @@ struct ResetPasswordTemplate {
77 89 app_description: String,
78 90 page_title: String,
79 91 reset_password_title: String,
92 + subtitle: String,
80 93 new_password_label: String,
81 94 new_password_placeholder: String,
95 + confirm_password_label: String,
96 + confirm_password_placeholder: String,
82 97 password_requirements: String,
83 98 submit_button: String,
84 99 back_to_login: String,
85 100 success_message: String,
86 101 error_message: String,
102 + missing_token_url: String,
103 + missing_token: String,
104 + missing_new_password: String,
105 + passwords_not_match: String,
106 + reset_success_message: String,
107 + error_link_invalid: String,
108 + error_generic: String,
87 109 }
88 110
89 111 crate::impl_template_response!(LoginTemplate);
@@ -93,6 +115,7 @@ crate::impl_template_response!(ResetPasswordTemplate);
93 115 /// Handler for GET /login - Login page (PUBLIC)
94 116 pub async fn login_page(
95 117 headers: HeaderMap,
118 + Extension(i18n_service): Extension<Arc<I18nService>>,
96 119 ) -> Result<impl IntoResponse, crate::error::AppError> {
97 120 tracing::debug!("Rendering login page");
98 121
@@ -110,12 +133,113 @@ pub async fn login_page(
110 133 .unwrap_or_else(|| "fr".to_string());
111 134
112 135 let supported_languages: Vec<(String, String, bool)> = vec![
113 - ("fr".to_string(), "Français".to_string(), current_locale == "fr"),
114 - ("en".to_string(), "English".to_string(), current_locale == "en"),
115 - ("de".to_string(), "Deutsch".to_string(), current_locale == "de"),
116 - ("es".to_string(), "Español".to_string(), current_locale == "es"),
136 + (
137 + "fr".to_string(),
138 + "Français".to_string(),
139 + current_locale == "fr",
140 + ),
141 + (
142 + "en".to_string(),
143 + "English".to_string(),
144 + current_locale == "en",
145 + ),
146 + (
147 + "de".to_string(),
148 + "Deutsch".to_string(),
149 + current_locale == "de",
150 + ),
151 + (
152 + "es".to_string(),
153 + "Español".to_string(),
154 + current_locale == "es",
155 + ),
117 156 ];
118 157
158 + let locale_str = &current_locale;
159 + let page_title = i18n_service.translate("login.page_title", locale_str).await;
160 + let page_description = i18n_service
161 + .translate("login.page_description", locale_str)
162 + .await;
163 + let subtitle = i18n_service.translate("login.subtitle", locale_str).await;
164 + let tab_login = i18n_service.translate("login.tab_login", locale_str).await;
165 + let tab_register = i18n_service
166 + .translate("login.tab_register", locale_str)
167 + .await;
168 + let login_title = i18n_service.translate("login.title", locale_str).await;
169 + let register_title = i18n_service
170 + .translate("login.register_title", locale_str)
171 + .await;
172 + let username_label = i18n_service.translate("login.username", locale_str).await;
173 + let password_label = i18n_service.translate("login.password", locale_str).await;
174 + let email_label = i18n_service.translate("login.email_label", locale_str).await;
175 + let username_placeholder = i18n_service
176 + .translate("login.username_placeholder", locale_str)
177 + .await;
178 + let password_placeholder = i18n_service
179 + .translate("login.password_placeholder", locale_str)
180 + .await;
181 + let username_placeholder_register = i18n_service
182 + .translate("login.username_placeholder_register", locale_str)
183 + .await;
184 + let password_placeholder_register = i18n_service
185 + .translate("login.password_placeholder_register", locale_str)
186 + .await;
187 + let email_placeholder = i18n_service
188 + .translate("login.email_placeholder", locale_str)
189 + .await;
190 + let submit_button = i18n_service.translate("login.submit", locale_str).await;
191 + let register_submit = i18n_service
192 + .translate("login.register_submit", locale_str)
193 + .await;
194 + let remember_me = i18n_service
195 + .translate("login.remember_me", locale_str)
196 + .await;
197 + let forgot_password = i18n_service
198 + .translate("login.forgot_password", locale_str)
199 + .await;
200 + let email_not_verified = i18n_service
201 + .translate("login.email_not_verified", locale_str)
202 + .await;
203 + let resend_verification = i18n_service
204 + .translate("login.resend_verification", locale_str)
205 + .await;
206 + let already_have_account = i18n_service
207 + .translate("login.already_have_account", locale_str)
208 + .await;
209 + let sign_in_link = i18n_service
210 + .translate("login.sign_in_link", locale_str)
211 + .await;
212 + let footer_text = i18n_service
213 + .translate("login.footer_text", locale_str)
214 + .await;
215 + let success_message = i18n_service
216 + .translate("login.success_message", locale_str)
217 + .await;
218 + let error_message = i18n_service
219 + .translate("login.error_message", locale_str)
220 + .await;
221 + let email_unverified_message = i18n_service
222 + .translate("login.email_unverified_message", locale_str)
223 + .await;
224 + let resend_button = i18n_service
225 + .translate("login.resend_button", locale_str)
226 + .await;
227 + let resend_success = i18n_service
228 + .translate("login.resend_success", locale_str)
229 + .await;
230 + let resend_error = i18n_service
231 + .translate("login.resend_error", locale_str)
232 + .await;
233 + let resend_prompt = i18n_service
234 + .translate("login.resend_prompt", locale_str)
235 + .await;
236 + let register_success = i18n_service
237 + .translate("login.register_success", locale_str)
238 + .await;
239 + let register_error = i18n_service
240 + .translate("login.register_error", locale_str)
241 + .await;
242 +
119 243 let layout = LayoutContext::new("", "");
120 244
121 245 let template = LoginTemplate {
@@ -123,39 +247,39 @@ pub async fn login_page(
123 247 app_version: layout.app_version.clone(),
124 248 current_locale,
125 249 supported_languages,
126 - page_title: "Connexion - RustWarden".to_string(),
127 - page_description: "Gestionnaire de mots de passe sécurisé".to_string(),
128 - subtitle: "Connectez-vous à votre compte".to_string(),
129 - tab_login: "Connexion".to_string(),
130 - tab_register: "Inscription".to_string(),
131 - login_title: "Connexion".to_string(),
132 - register_title: "Créer un compte".to_string(),
133 - username_label: "Nom d'utilisateur".to_string(),
134 - password_label: "Mot de passe".to_string(),
135 - email_label: "Email".to_string(),
136 - username_placeholder: "Entrez votre nom d'utilisateur".to_string(),
137 - password_placeholder: "Entrez votre mot de passe".to_string(),
138 - username_placeholder_register: "Choisissez un nom d'utilisateur".to_string(),
139 - password_placeholder_register: "Choisissez un mot de passe".to_string(),
140 - email_placeholder: "votre@email.com".to_string(),
141 - submit_button: "Se connecter".to_string(),
142 - register_submit: "S'inscrire".to_string(),
143 - remember_me: "Se souvenir de moi".to_string(),
144 - forgot_password: "Mot de passe oublié ?".to_string(),
145 - email_not_verified: "Votre email n'est pas vérifié".to_string(),
146 - resend_verification: "Renvoyer l'email de vérification".to_string(),
147 - already_have_account: "Vous avez déjà un compte ?".to_string(),
148 - sign_in_link: "Se connecter".to_string(),
149 - footer_text: "RustWarden - Gestionnaire de mots de passe sécurisé".to_string(),
150 - success_message: "Connexion réussie ! Redirection...".to_string(),
151 - error_message: "Identifiants invalides".to_string(),
152 - email_unverified_message: "Votre email n'est pas vérifié. Veuillez vérifier votre boîte de réception.".to_string(),
153 - resend_button: "Renvoyer".to_string(),
154 - resend_success: "Email de vérification renvoyé avec succès".to_string(),
155 - resend_error: "Erreur lors de l'envoi de l'email".to_string(),
156 - resend_prompt: "Veuillez entrer votre email pour renvoyer le lien de vérification".to_string(),
157 - register_success: "Inscription réussie ! Veuillez vérifier votre email.".to_string(),
158 - register_error: "Erreur lors de l'inscription".to_string(),
250 + page_title,
251 + page_description,
252 + subtitle,
253 + tab_login,
254 + tab_register,
255 + login_title,
256 + register_title,
257 + username_label,
258 + password_label,
259 + email_label,
260 + username_placeholder,
261 + password_placeholder,
262 + username_placeholder_register,
263 + password_placeholder_register,
264 + email_placeholder,
265 + submit_button,
266 + register_submit,
267 + remember_me,
268 + forgot_password,
269 + email_not_verified,
270 + resend_verification,
271 + already_have_account,
272 + sign_in_link,
273 + footer_text,
274 + success_message,
275 + error_message,
276 + email_unverified_message,
277 + resend_button,
278 + resend_success,
279 + resend_error,
280 + resend_prompt,
281 + register_success,
282 + register_error,
159 283 };
160 284
161 285 Ok(template)
@@ -163,17 +287,66 @@ pub async fn login_page(
163 287
164 288 /// Handler for GET /resend-verification - Resend email verification page (PUBLIC)
165 289 pub async fn resend_verification_page(
166 - State(_db): State<DatabaseConnection>,
290 + headers: HeaderMap,
291 + State(db): State<DatabaseConnection>,
292 + Extension(i18n_service): Extension<Arc<I18nService>>,
167 293 ) -> Result<impl IntoResponse, crate::error::AppError> {
168 294 tracing::debug!("Rendering resend verification page");
169 295
170 - let layout = LayoutContext::new("Renvoyer l'email de validation", "");
296 + let jar = CookieJar::from_headers(&headers);
297 + let current_locale = jar
298 + .get("locale")
299 + .and_then(|c| {
300 + let value = c.value();
301 + if matches!(value, "fr" | "en" | "de" | "es") {
302 + Some(value.to_string())
303 + } else {
304 + None
305 + }
306 + })
307 + .unwrap_or_else(|| "fr".to_string());
308 +
309 + let locale_str = &current_locale;
310 + let page_title = i18n_service.translate("resend_verification.page_title", locale_str).await;
311 + let title = i18n_service.translate("resend_verification.title", locale_str).await;
312 + let subtitle = i18n_service.translate("resend_verification.subtitle", locale_str).await;
313 + let email_label = i18n_service.translate("resend_verification.email_label", locale_str).await;
314 + let email_placeholder = i18n_service.translate("resend_verification.email_placeholder", locale_str).await;
315 + let submit = i18n_service.translate("resend_verification.submit", locale_str).await;
316 + let back_to_login = i18n_service.translate("resend_verification.back_to_login", locale_str).await;
317 + let error_default = i18n_service.translate("resend_verification.error_default", locale_str).await;
318 + let success_default = i18n_service.translate("resend_verification.success_default", locale_str).await;
319 + let error_missing_email = i18n_service.translate("resend_verification.error_missing_email", locale_str).await;
320 + let error_cannot_send = i18n_service.translate("resend_verification.error_cannot_send", locale_str).await;
321 +
322 + let layout = LayoutContext::new("", "");
323 +
324 + use crate::services::app_settings_service::AppSettingsService;
325 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
326 + .await?
327 + .map(|s| s.value)
328 + .unwrap_or_else(|| "localhost".to_string());
329 + let app_description = AppSettingsService::get_setting(&db, "app_description")
330 + .await?
331 + .map(|s| s.value)
332 + .unwrap_or_else(|| "Secure password manager".to_string());
171 333
172 334 let template = ResendVerificationTemplate {
173 335 app_name: layout.app_name.clone(),
174 336 app_version: layout.app_version.clone(),
175 - app_domain: "localhost".to_string(),
176 - app_description: "Secure password manager".to_string(),
337 + app_domain,
338 + app_description,
339 + page_title,
340 + title,
341 + subtitle,
342 + email_label,
343 + email_placeholder,
344 + submit,
345 + back_to_login,
346 + error_default,
347 + success_default,
348 + error_missing_email,
349 + error_cannot_send,
177 350 };
178 351
179 352 Ok(template)
@@ -188,28 +361,119 @@ pub struct ResetPasswordQuery {
188 361
189 362 /// Handler for GET /reset-password?token={token} - Reset password page (PUBLIC)
190 363 pub async fn reset_password_page(
191 - _headers: HeaderMap,
192 - State(_db): State<DatabaseConnection>,
364 + headers: HeaderMap,
365 + State(db): State<DatabaseConnection>,
366 + Extension(i18n_service): Extension<Arc<I18nService>>,
193 367 axum::extract::Query(_params): axum::extract::Query<ResetPasswordQuery>,
194 368 ) -> Result<impl IntoResponse, crate::error::AppError> {
195 369 tracing::debug!("Rendering reset password page");
196 370
371 + let jar = CookieJar::from_headers(&headers);
372 + let current_locale = jar
373 + .get("locale")
374 + .and_then(|c| {
375 + let value = c.value();
376 + if matches!(value, "fr" | "en" | "de" | "es") {
377 + Some(value.to_string())
378 + } else {
379 + None
380 + }
381 + })
382 + .unwrap_or_else(|| "fr".to_string());
383 +
384 + let locale_str = &current_locale;
385 + let page_title = i18n_service
386 + .translate("password_reset.page_title", locale_str)
387 + .await;
388 + let reset_password_title = i18n_service
389 + .translate("password_reset.title", locale_str)
390 + .await;
391 + let subtitle = i18n_service
392 + .translate("password_reset.subtitle", locale_str)
393 + .await;
394 + let new_password_label = i18n_service
395 + .translate("password_reset.new_password_label", locale_str)
396 + .await;
397 + let new_password_placeholder = i18n_service
398 + .translate("password_reset.new_password_placeholder", locale_str)
399 + .await;
400 + let confirm_password_label = i18n_service
401 + .translate("password_reset.confirm_password_label", locale_str)
402 + .await;
403 + let confirm_password_placeholder = i18n_service
404 + .translate("password_reset.confirm_password_placeholder", locale_str)
405 + .await;
406 + let password_requirements = i18n_service
407 + .translate("password_reset.requirements", locale_str)
408 + .await;
409 + let submit_button = i18n_service.translate("login.submit", locale_str).await;
410 + let back_to_login = i18n_service
411 + .translate("login.sign_in_link", locale_str)
412 + .await;
413 + let success_message = i18n_service
414 + .translate("login.success_message", locale_str)
415 + .await;
416 + let error_message = i18n_service
417 + .translate("login.error_message", locale_str)
418 + .await;
419 + let missing_token_url = i18n_service
420 + .translate("password_reset.missing_token_url", locale_str)
421 + .await;
422 + let missing_token = i18n_service
423 + .translate("password_reset.missing_token", locale_str)
424 + .await;
425 + let missing_new_password = i18n_service
426 + .translate("password_reset.missing_new_password", locale_str)
427 + .await;
428 + let passwords_not_match = i18n_service
429 + .translate("password_reset.passwords_not_match", locale_str)
430 + .await;
431 + let reset_success_message = i18n_service
432 + .translate("password_reset.success_message", locale_str)
433 + .await;
434 + let error_link_invalid = i18n_service
435 + .translate("password_reset.error_link_invalid", locale_str)
436 + .await;
437 + let error_generic = i18n_service
438 + .translate("password_reset.error_generic", locale_str)
439 + .await;
440 +
197 441 let layout = LayoutContext::new("", "");
198 442
443 + use crate::services::app_settings_service::AppSettingsService;
444 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
445 + .await?
446 + .map(|s| s.value)
447 + .unwrap_or_else(|| "localhost".to_string());
448 + let app_description = AppSettingsService::get_setting(&db, "app_description")
449 + .await?
450 + .map(|s| s.value)
451 + .unwrap_or_else(|| "Secure password manager".to_string());
452 +
199 453 let template = ResetPasswordTemplate {
200 454 app_name: layout.app_name.clone(),
201 455 app_version: layout.app_version.clone(),
202 - app_domain: "localhost".to_string(),
203 - app_description: "Secure password manager".to_string(),
204 - page_title: "Réinitialiser le mot de passe".to_string(),
205 - reset_password_title: "Réinitialiser le mot de passe".to_string(),
206 - new_password_label: "Nouveau mot de passe".to_string(),
207 - new_password_placeholder: "Entrez le nouveau mot de passe".to_string(),
208 - password_requirements: "Le mot de passe doit contenir au moins 8 caractères".to_string(),
209 - submit_button: "Réinitialiser".to_string(),
210 - back_to_login: "Retour à la connexion".to_string(),
211 - success_message: "Mot de passe réinitialisé avec succès".to_string(),
212 - error_message: "Erreur lors de la réinitialisation".to_string(),
456 + app_domain,
457 + app_description,
458 + page_title,
459 + reset_password_title,
460 + subtitle,
461 + new_password_label,
462 + new_password_placeholder,
463 + confirm_password_label,
464 + confirm_password_placeholder,
465 + password_requirements,
466 + submit_button,
467 + back_to_login,
468 + success_message,
469 + error_message,
470 + missing_token_url,
471 + missing_token,
472 + missing_new_password,
473 + passwords_not_match,
474 + reset_success_message,
475 + error_link_invalid,
476 + error_generic,
213 477 };
214 478
215 479 Ok(template)
M src/api/handlers/settings_handler.rs
+340 -266
@@ -10,11 +10,17 @@ use axum::{
10 10 };
11 11 use sea_orm::DatabaseConnection;
12 12 use serde::Deserialize;
13 +use std::sync::Arc;
13 14 use tower_sessions::Session;
14 15
16 +use crate::api::dto::navigation_dto::NavigationDTO;
17 +use crate::api::dto::user_profile_dto::UserProfileDTO;
15 18 use crate::auth::jwt::Claims;
16 19 use crate::config::email::EmailConfig;
17 20 use crate::error::AppError;
21 +use crate::i18n::extractor::Locale;
22 +use crate::i18n::service::I18nService;
23 +use crate::services::app_settings_service::AppSettingsService;
18 24 use crate::services::refresh_token_service::RefreshTokenService;
19 25 use crate::services::user_service::UserService;
20 26 use chrono::Utc;
@@ -69,98 +75,24 @@ struct NavItem {
69 75 has_disabled_message: bool,
70 76 }
71 77
72 -fn build_nav_items(roles: &[String], active_id: &str) -> Vec<NavItem> {
73 - let mut items = vec![
74 - NavItem {
75 - id: "home".to_string(),
76 - label: "Accueil".to_string(),
77 - icon: "home".to_string(),
78 - url: "/home".to_string(),
79 - active: active_id == "home",
80 - enabled: true,
81 - badge: String::new(),
82 - has_badge: false,
83 - disabled_message: String::new(),
84 - has_disabled_message: false,
85 - },
86 - NavItem {
87 - id: "settings".to_string(),
88 - label: "Paramètres".to_string(),
89 - icon: "settings".to_string(),
90 - url: "/settings".to_string(),
91 - active: active_id == "settings",
92 - enabled: true,
93 - badge: String::new(),
94 - has_badge: false,
95 - disabled_message: String::new(),
96 - has_disabled_message: false,
97 - },
98 - ];
99 -
100 - if roles.contains(&"admin".to_string()) {
101 - items.push(NavItem {
102 - id: "admin".to_string(),
103 - label: "Administration".to_string(),
104 - icon: "shield".to_string(),
105 - url: "/admin".to_string(),
106 - active: active_id == "admin",
107 - enabled: true,
108 - badge: String::new(),
109 - has_badge: false,
110 - disabled_message: String::new(),
111 - has_disabled_message: false,
112 - });
113 - }
114 -
115 - items
116 -}
117 -
118 -/// Helper to build a simple UserProfile from user model + roles
119 -struct SimpleUserProfile {
120 - id: uuid::Uuid,
121 - username: String,
122 - email: String,
123 - initials: String,
124 - avatar_color: String,
125 - roles: Vec<String>,
126 - created_at: chrono::DateTime<chrono::Utc>,
127 -}
128 -
129 -impl SimpleUserProfile {
130 - fn from_model(model: &crate::models::user::Model, roles: Vec<String>) -> Self {
131 - let initials = {
132 - let parts: Vec<&str> = model.username.split_whitespace().collect();
133 - match parts.len() {
134 - 0 => "??".to_string(),
135 - 1 => model.username.chars().take(2).collect::<String>().to_uppercase(),
136 - _ => format!(
137 - "{}{}",
138 - parts[0].chars().next().unwrap_or('?'),
139 - parts[1].chars().next().unwrap_or('?')
140 - ).to_uppercase(),
141 - }
142 - };
143 -
144 - let avatar_color = {
145 - let hash: u32 = model.username
146 - .bytes()
147 - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
148 - let hue = hash % 180;
149 - let sat = 60 + (hash % 20) as u8;
150 - let light = 45 + (hash % 15) as u8;
151 - format!("hsl({}, {}%, {}%)", hue, sat, light)
152 - };
153 -
154 - Self {
155 - id: model.id,
156 - username: model.username.clone(),
157 - email: model.email.clone(),
158 - initials,
159 - avatar_color,
160 - roles,
161 - created_at: model.created_at,
162 - }
163 - }
78 +/// Helper to build NavItems from NavigationDTO
79 +fn build_nav_items_from_dto(navigation: &NavigationDTO, active_id: &str) -> Vec<NavItem> {
80 + navigation
81 + .items
82 + .iter()
83 + .map(|item| NavItem {
84 + id: item.id.clone(),
85 + label: item.label.clone(),
86 + icon: item.icon.clone(),
87 + url: item.url.clone(),
88 + active: item.id == active_id,
89 + enabled: item.enabled,
90 + badge: item.badge.clone(),
91 + has_badge: item.has_badge,
92 + disabled_message: item.disabled_message.clone(),
93 + has_disabled_message: item.has_disabled_message,
94 + })
95 + .collect()
164 96 }
165 97
166 98 /// Settings page template
@@ -184,6 +116,21 @@ struct SettingsTemplate {
184 116 user_menu_profile: String,
185 117 user_menu_logout: String,
186 118 user_menu_settings: String,
119 + settings_title: String,
120 + settings_subtitle: String,
121 + profile_title: String,
122 + profile_desc: String,
123 + password_title: String,
124 + password_desc: String,
125 + sessions_title: String,
126 + sessions_desc: String,
127 + preferences_title: String,
128 + preferences_desc: String,
129 + access_button: String,
130 + info_title: String,
131 + info_desc: String,
132 + breadcrumb_home: String,
133 + breadcrumb_settings: String,
187 134 }
188 135
189 136 crate::impl_template_response!(SettingsTemplate);
@@ -191,6 +138,8 @@ crate::impl_template_response!(SettingsTemplate);
191 138 /// Handler for GET /settings - Settings page (PROTECTED)
192 139 pub async fn settings(
193 140 Extension(claims): Extension<Claims>,
141 + Extension(i18n_service): Extension<Arc<I18nService>>,
142 + Extension(locale): Extension<Locale>,
194 143 State(db): State<DatabaseConnection>,
195 144 ) -> Result<impl IntoResponse, AppError> {
196 145 let username = claims.username.clone();
@@ -204,18 +153,52 @@ pub async fn settings(
204 153 })?;
205 154
206 155 let roles = UserService::get_user_roles(&db, user_model.id).await?;
207 - let user = SimpleUserProfile::from_model(&user_model, roles.clone());
208 -
209 - let layout = crate::templates::LayoutContext::new("Paramètres", "");
210 - let nav_items = build_nav_items(&roles, "settings");
156 + let user = UserProfileDTO::new(user_model, roles.clone());
157 +
158 + let locale_str = locale.as_str();
159 +
160 + let user_menu_profile = i18n_service.translate("user_menu.profile", locale_str).await;
161 + let user_menu_logout = i18n_service.translate("user_menu.logout", locale_str).await;
162 + let user_menu_settings = i18n_service.translate("user_menu.settings", locale_str).await;
163 +
164 + let settings_page_title = i18n_service.translate("settings.page_title", locale_str).await;
165 + let settings_title = i18n_service.translate("settings.title", locale_str).await;
166 + let settings_subtitle = i18n_service.translate("settings.subtitle", locale_str).await;
167 + let profile_title = i18n_service.translate("settings.profile_title", locale_str).await;
168 + let profile_desc = i18n_service.translate("settings.profile_desc", locale_str).await;
169 + let password_title = i18n_service.translate("settings.password_title", locale_str).await;
170 + let password_desc = i18n_service.translate("settings.password_desc", locale_str).await;
171 + let sessions_title = i18n_service.translate("settings.sessions_title", locale_str).await;
172 + let sessions_desc = i18n_service.translate("settings.sessions_desc", locale_str).await;
173 + let preferences_title = i18n_service.translate("settings.preferences_title", locale_str).await;
174 + let preferences_desc = i18n_service.translate("settings.preferences_desc", locale_str).await;
175 + let access_button = i18n_service.translate("settings.access", locale_str).await;
176 + let info_title = i18n_service.translate("settings.info_title", locale_str).await;
177 + let info_desc = i18n_service.translate("settings.info_desc", locale_str).await;
178 + let breadcrumb_home = i18n_service.translate("breadcrumb.home", locale_str).await;
179 + let breadcrumb_settings = i18n_service.translate("breadcrumb.settings", locale_str).await;
180 +
181 + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
182 + let nav_items = build_nav_items_from_dto(&navigation, "settings");
183 +
184 + let layout = crate::templates::LayoutContext::new("", "");
185 +
186 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
187 + .await?
188 + .map(|s| s.value)
189 + .unwrap_or_else(|| "localhost".to_string());
190 + let app_description = AppSettingsService::get_setting(&db, "app_description")
191 + .await?
192 + .map(|s| s.value)
193 + .unwrap_or_else(|| "Secure password manager".to_string());
211 194
212 195 let template = SettingsTemplate {
213 - page_title: layout.page_title.clone(),
196 + page_title: settings_page_title,
214 197 csrf_token: layout.csrf_token.clone(),
215 198 app_name: layout.app_name.clone(),
216 199 app_version: layout.app_version.clone(),
217 - app_domain: "localhost".to_string(),
218 - app_description: "Secure password manager".to_string(),
200 + app_domain,
201 + app_description,
219 202 username: user.username.clone(),
220 203 email: user.email.clone(),
221 204 initials: user.initials.clone(),
@@ -223,9 +206,24 @@ pub async fn settings(
223 206 roles_text: user.roles.join(", "),
224 207 nav_items,
225 208 has_pending_password_change: false,
226 - user_menu_profile: "Profil".to_string(),
227 - user_menu_logout: "Déconnexion".to_string(),
228 - user_menu_settings: "Paramètres".to_string(),
209 + user_menu_profile,
210 + user_menu_logout,
211 + user_menu_settings,
212 + settings_title,
213 + settings_subtitle,
214 + profile_title,
215 + profile_desc,
216 + password_title,
217 + password_desc,
218 + sessions_title,
219 + sessions_desc,
220 + preferences_title,
221 + preferences_desc,
222 + access_button,
223 + info_title,
224 + info_desc,
225 + breadcrumb_home,
226 + breadcrumb_settings,
229 227 };
230 228
231 229 Ok(template)
@@ -644,7 +642,7 @@ pub async fn update_preferences(
644 642 }
645 643
646 644 // ============================================================================
647 -// GET Handlers for HTML Pages (simplified - no i18n)
645 +// GET Handlers for HTML Pages (with i18n)
648 646 // ============================================================================
649 647
650 648 /// Custom filters for Askama templates
@@ -949,6 +947,8 @@ crate::impl_template_response!(PreferencesTemplate);
949 947 /// Handler for GET /profile - Display profile page (PROTECTED)
950 948 pub async fn get_profile_page(
951 949 Extension(claims): Extension<Claims>,
950 + Extension(i18n_service): Extension<Arc<I18nService>>,
951 + Extension(locale): Extension<Locale>,
952 952 State(db): State<DatabaseConnection>,
953 953 session: Session,
954 954 ) -> Result<impl IntoResponse, AppError> {
@@ -957,22 +957,34 @@ pub async fn get_profile_page(
957 957 .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
958 958
959 959 let roles = UserService::get_user_roles(&db, user_model.id).await?;
960 - let user = SimpleUserProfile::from_model(&user_model, roles.clone());
960 + let user = UserProfileDTO::new(user_model, roles.clone());
961 +
962 + let locale_str = locale.as_str();
961 963
962 964 let layout = crate::templates::LayoutContext::new("Profil", "");
963 965
964 966 use crate::templates::helpers::csrf_token_helper;
965 967 let csrf_token = csrf_token_helper(&session).await;
966 968
967 - let nav_items = build_nav_items(&roles, "settings");
969 + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
970 + let nav_items = build_nav_items_from_dto(&navigation, "settings");
971 +
972 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
973 + .await?
974 + .map(|s| s.value)
975 + .unwrap_or_else(|| "localhost".to_string());
976 + let app_description = AppSettingsService::get_setting(&db, "app_description")
977 + .await?
978 + .map(|s| s.value)
979 + .unwrap_or_else(|| "Secure password manager".to_string());
968 980
969 981 let template = ProfileTemplate {
970 982 page_title: layout.page_title.clone(),
971 983 csrf_token,
972 984 app_name: layout.app_name.clone(),
973 985 app_version: layout.app_version.clone(),
974 - app_domain: "localhost".to_string(),
975 - app_description: "Secure password manager".to_string(),
986 + app_domain,
987 + app_description,
976 988 username: user.username.clone(),
977 989 email: user.email.clone(),
978 990 user_id: user.id.to_string(),
@@ -983,30 +995,30 @@ pub async fn get_profile_page(
983 995 avatar_color: user.avatar_color.clone(),
984 996 nav_items,
985 997 has_pending_password_change: false,
986 - user_menu_profile: "Profil".to_string(),
987 - user_menu_logout: "Déconnexion".to_string(),
988 - user_menu_settings: "Paramètres".to_string(),
989 - title: "Profil".to_string(),
990 - subtitle: "Gérez vos informations personnelles".to_string(),
991 - account_info: "Informations du compte".to_string(),
992 - username_label: "Nom d'utilisateur".to_string(),
993 - username_public: "Visible publiquement".to_string(),
994 - username_placeholder: "Entrez votre nom d'utilisateur".to_string(),
995 - username_pattern: "Lettres, chiffres, tirets et underscores".to_string(),
996 - username_help: "3 à 30 caractères".to_string(),
997 - email_label: "Email".to_string(),
998 - email_used_for: "Utilisé pour les notifications".to_string(),
999 - email_warning: "La modification de l'email nécessite une vérification".to_string(),
1000 - email_placeholder: "votre@email.com".to_string(),
1001 - email_change_info: "Un email de vérification sera envoyé à la nouvelle adresse".to_string(),
1002 - save_button: "Enregistrer".to_string(),
1003 - cancel: "Annuler".to_string(),
1004 - additional_info: "Informations complémentaires".to_string(),
1005 - account_created: "Compte créé le".to_string(),
1006 - roles_label: "Rôles".to_string(),
1007 - breadcrumb_home: "Accueil".to_string(),
1008 - breadcrumb_settings: "Paramètres".to_string(),
1009 - breadcrumb_profile: "Profil".to_string(),
998 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
999 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1000 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1001 + title: i18n_service.translate("profile.title", locale_str).await,
1002 + subtitle: i18n_service.translate("profile.subtitle", locale_str).await,
1003 + account_info: i18n_service.translate("profile.account_info", locale_str).await,
1004 + username_label: i18n_service.translate("profile.username", locale_str).await,
1005 + username_public: i18n_service.translate("profile.username_public", locale_str).await,
1006 + username_placeholder: i18n_service.translate("profile.username_placeholder", locale_str).await,
1007 + username_pattern: i18n_service.translate("profile.username_pattern", locale_str).await,
1008 + username_help: i18n_service.translate("profile.username_help", locale_str).await,
1009 + email_label: i18n_service.translate("profile.email_label", locale_str).await,
1010 + email_used_for: i18n_service.translate("profile.email_used_for", locale_str).await,
1011 + email_warning: i18n_service.translate("profile.email_warning", locale_str).await,
1012 + email_placeholder: i18n_service.translate("profile.email_placeholder", locale_str).await,
1013 + email_change_info: i18n_service.translate("profile.email_change_info", locale_str).await,
1014 + save_button: i18n_service.translate("profile.save", locale_str).await,
1015 + cancel: i18n_service.translate("common.cancel", locale_str).await,
1016 + additional_info: i18n_service.translate("profile.additional_info", locale_str).await,
1017 + account_created: i18n_service.translate("profile.account_created", locale_str).await,
1018 + roles_label: i18n_service.translate("profile.roles", locale_str).await,
1019 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1020 + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await,
1021 + breadcrumb_profile: i18n_service.translate("breadcrumb.profile", locale_str).await,
1010 1022 };
1011 1023
1012 1024 Ok(template)
@@ -1015,6 +1027,8 @@ pub async fn get_profile_page(
1015 1027 /// Handler for GET /settings/password - Display password change page (PROTECTED)
1016 1028 pub async fn get_password_page(
1017 1029 Extension(claims): Extension<Claims>,
1030 + Extension(i18n_service): Extension<Arc<I18nService>>,
1031 + Extension(locale): Extension<Locale>,
1018 1032 State(db): State<DatabaseConnection>,
1019 1033 session: Session,
1020 1034 Query(query): Query<PasswordPageQuery>,
@@ -1024,11 +1038,13 @@ pub async fn get_password_page(
1024 1038 .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
1025 1039
1026 1040 let roles = UserService::get_user_roles(&db, user_model.id).await?;
1027 - let user = SimpleUserProfile::from_model(&user_model, roles.clone());
1041 + let user = UserProfileDTO::new(user_model.clone(), roles.clone());
1042 +
1043 + let locale_str = locale.as_str();
1028 1044
1029 1045 let must_change = query.must_change.as_deref() == Some("true");
1030 1046 let must_change_message = if must_change {
1031 - "Vous devez changer votre mot de passe avant de continuer.".to_string()
1047 + i18n_service.translate("password.must_change_message", locale_str).await
1032 1048 } else {
1033 1049 String::new()
1034 1050 };
@@ -1041,76 +1057,102 @@ pub async fn get_password_page(
1041 1057 let has_pending_request = pending_request.is_some();
1042 1058 let has_pending_password_change = has_pending_request;
1043 1059
1060 + let pending_request_message = if has_pending_request {
1061 + i18n_service.translate("password.pending_request_message", locale_str).await
1062 + } else {
1063 + String::new()
1064 + };
1065 + let pending_request_info = if has_pending_request {
1066 + i18n_service.translate("password.pending_request_info", locale_str).await
1067 + } else {
1068 + String::new()
1069 + };
1070 + let pending_request_cancel = i18n_service.translate("password.pending_request_cancel", locale_str).await;
1071 +
1044 1072 let layout = crate::templates::LayoutContext::new("Mot de passe", "");
1045 1073 use crate::templates::helpers::csrf_token_helper;
1046 1074 let csrf_token = csrf_token_helper(&session).await;
1047 - let nav_items = build_nav_items(&roles, "settings");
1075 +
1076 + let mut navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
1077 + navigation.set_pending_password_change(has_pending_password_change);
1078 + let nav_items = build_nav_items_from_dto(&navigation, "settings");
1079 +
1080 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
1081 + .await?
1082 + .map(|s| s.value)
1083 + .unwrap_or_else(|| "localhost".to_string());
1084 + let app_description = AppSettingsService::get_setting(&db, "app_description")
1085 + .await?
1086 + .map(|s| s.value)
1087 + .unwrap_or_else(|| "Secure password manager".to_string());
1088 +
1089 + let page_title = i18n_service.translate("password.page_title", locale_str).await;
1048 1090
1049 1091 let template = PasswordTemplate {
1050 - page_title: "Changer le mot de passe".to_string(),
1092 + page_title,
1051 1093 csrf_token,
1052 1094 app_name: layout.app_name.clone(),
1053 1095 app_version: layout.app_version.clone(),
1054 - app_domain: "localhost".to_string(),
1055 - app_description: "Secure password manager".to_string(),
1096 + app_domain,
1097 + app_description,
1056 1098 username: user.username.clone(),
1057 1099 email: user.email.clone(),
1058 1100 roles_text: user.roles.join(", "),
1059 1101 initials: user.initials.clone(),
1060 1102 avatar_color: user.avatar_color.clone(),
1061 1103 nav_items,
1062 - user_menu_profile: "Profil".to_string(),
1063 - user_menu_logout: "Déconnexion".to_string(),
1064 - user_menu_settings: "Paramètres".to_string(),
1104 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
1105 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1106 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1065 1107 must_change,
1066 1108 must_change_message,
1067 1109 has_pending_request,
1068 - pending_request_message: if has_pending_request { "Une demande de changement de mot de passe est en attente de confirmation.".to_string() } else { String::new() },
1069 - pending_request_info: if has_pending_request { "Vérifiez votre boîte de réception pour le lien de confirmation.".to_string() } else { String::new() },
1070 - pending_request_cancel: "Annuler la demande".to_string(),
1071 - title: "Changer le mot de passe".to_string(),
1072 - subtitle: "Modifiez votre mot de passe de connexion".to_string(),
1073 - new_password_label: "Nouveau mot de passe".to_string(),
1074 - current_password_label: "Mot de passe actuel".to_string(),
1075 - current_password_placeholder: "Entrez votre mot de passe actuel".to_string(),
1076 - new_password_placeholder: "Entrez le nouveau mot de passe".to_string(),
1077 - required: "Requis".to_string(),
1078 - requirements: "Au moins 8 caractères, une majuscule, une minuscule et un chiffre".to_string(),
1079 - strength_label: "Force du mot de passe".to_string(),
1080 - strength_weak: "Faible".to_string(),
1081 - strength_medium: "Moyen".to_string(),
1082 - strength_strong: "Fort".to_string(),
1083 - strength_too_short: "Trop court".to_string(),
1084 - strength_missing_lowercase: "Ajoutez une minuscule".to_string(),
1085 - strength_missing_uppercase: "Ajoutez une majuscule".to_string(),
1086 - strength_missing_digit: "Ajoutez un chiffre".to_string(),
1087 - strength_missing_special: "Ajoutez un caractère spécial".to_string(),
1088 - confirm_password_label: "Confirmer le mot de passe".to_string(),
1089 - confirm_password_placeholder: "Confirmez le nouveau mot de passe".to_string(),
1090 - passwords_match: "Les mots de passe correspondent".to_string(),
1091 - passwords_not_match: "Les mots de passe ne correspondent pas".to_string(),
1092 - change_button: "Changer le mot de passe".to_string(),
1093 - changing: "Changement en cours...".to_string(),
1094 - logout_warning_title: "Attention".to_string(),
1095 - logout_warning_desc: "Changer votre mot de passe invalidera toutes vos sessions actives.".to_string(),
1096 - saved: "Mot de passe modifié avec succès".to_string(),
1097 - error_all_fields_required: "Tous les champs sont requis".to_string(),
1098 - error_empty_data: "Les données sont vides".to_string(),
1099 - error_invalid_data: "Données invalides".to_string(),
1100 - error_incorrect_current: "Le mot de passe actuel est incorrect".to_string(),
1101 - error_reading_response: "Erreur de lecture de la réponse".to_string(),
1102 - error_415: "Type de contenu non supporté".to_string(),
1103 - network_error: "Erreur réseau".to_string(),
1104 - security_tips_title: "Conseils de sécurité".to_string(),
1105 - security_tip_unique: "Utilisez un mot de passe unique pour chaque service".to_string(),
1106 - security_tip_share: "Ne partagez jamais votre mot de passe".to_string(),
1107 - security_tip_regular: "Changez régulièrement votre mot de passe".to_string(),
1108 - security_tip_2fa: "Activez l'authentification à deux facteurs si disponible".to_string(),
1109 - security_tip_manager: "Utilisez un gestionnaire de mots de passe".to_string(),
1110 - breadcrumb_home: "Accueil".to_string(),
1111 - breadcrumb_settings: "Paramètres".to_string(),
1112 - breadcrumb_password: "Mot de passe".to_string(),
1113 - cancel: "Annuler".to_string(),
1110 + pending_request_message,
1111 + pending_request_info,
1112 + pending_request_cancel,
1113 + title: i18n_service.translate("password.title", locale_str).await,
1114 + subtitle: i18n_service.translate("password.subtitle", locale_str).await,
1115 + new_password_label: i18n_service.translate("password.new_password", locale_str).await,
1116 + current_password_label: i18n_service.translate("password.current_password", locale_str).await,
1117 + current_password_placeholder: i18n_service.translate("password.current_password_placeholder", locale_str).await,
1118 + new_password_placeholder: i18n_service.translate("password.new_password_placeholder", locale_str).await,
1119 + required: i18n_service.translate("password.required", locale_str).await,
1120 + requirements: i18n_service.translate("password.requirements", locale_str).await,
1121 + strength_label: i18n_service.translate("password.strength", locale_str).await,
1122 + strength_weak: i18n_service.translate("password.strength_weak", locale_str).await,
1123 + strength_medium: i18n_service.translate("password.strength_medium", locale_str).await,
1124 + strength_strong: i18n_service.translate("password.strength_strong", locale_str).await,
1125 + strength_too_short: i18n_service.translate("password.strength_too_short", locale_str).await,
1126 + strength_missing_lowercase: i18n_service.translate("password.strength_missing_lowercase", locale_str).await,
1127 + strength_missing_uppercase: i18n_service.translate("password.strength_missing_uppercase", locale_str).await,
1128 + strength_missing_digit: i18n_service.translate("password.strength_missing_digit", locale_str).await,
1129 + strength_missing_special: i18n_service.translate("password.strength_missing_special", locale_str).await,
1130 + confirm_password_label: i18n_service.translate("password.confirm_password", locale_str).await,
1131 + confirm_password_placeholder: i18n_service.translate("password.confirm_password_placeholder", locale_str).await,
1132 + passwords_match: i18n_service.translate("password.passwords_match", locale_str).await,
1133 + passwords_not_match: i18n_service.translate("password.passwords_not_match", locale_str).await,
1134 + change_button: i18n_service.translate("password.change_button", locale_str).await,
1135 + changing: i18n_service.translate("password.changing", locale_str).await,
1136 + logout_warning_title: i18n_service.translate("password.logout_warning_title", locale_str).await,
1137 + logout_warning_desc: i18n_service.translate("password.logout_warning_desc", locale_str).await,
1138 + saved: i18n_service.translate("password.saved", locale_str).await,
1139 + error_all_fields_required: i18n_service.translate("password.error_all_fields_required", locale_str).await,
1140 + error_empty_data: i18n_service.translate("password.error_empty_data", locale_str).await,
1141 + error_invalid_data: i18n_service.translate("password.error_invalid_data", locale_str).await,
1142 + error_incorrect_current: i18n_service.translate("password.error_incorrect_current", locale_str).await,
1143 + error_reading_response: i18n_service.translate("password.error_reading_response", locale_str).await,
1144 + error_415: i18n_service.translate("password.error_415", locale_str).await,
1145 + network_error: i18n_service.translate("password.network_error", locale_str).await,
1146 + security_tips_title: i18n_service.translate("password.security_tips_title", locale_str).await,
1147 + security_tip_unique: i18n_service.translate("password.security_tip_unique", locale_str).await,
1148 + security_tip_share: i18n_service.translate("password.security_tip_share", locale_str).await,
1149 + security_tip_regular: i18n_service.translate("password.security_tip_regular", locale_str).await,
1150 + security_tip_2fa: i18n_service.translate("password.security_tip_2fa", locale_str).await,
1151 + security_tip_manager: i18n_service.translate("password.security_tip_manager", locale_str).await,
1152 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1153 + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await,
1154 + breadcrumb_password: i18n_service.translate("breadcrumb.password", locale_str).await,
1155 + cancel: i18n_service.translate("common.cancel", locale_str).await,
1114 1156 has_pending_password_change,
1115 1157 };
1116 1158
@@ -1120,6 +1162,8 @@ pub async fn get_password_page(
1120 1162 /// Handler for GET /settings/sessions - Display sessions page (PROTECTED)
1121 1163 pub async fn get_sessions_page(
1122 1164 Extension(claims): Extension<Claims>,
1165 + Extension(i18n_service): Extension<Arc<I18nService>>,
1166 + Extension(locale): Extension<Locale>,
1123 1167 State(db): State<DatabaseConnection>,
1124 1168 session: Session,
1125 1169 ) -> Result<impl IntoResponse, AppError> {
@@ -1128,7 +1172,9 @@ pub async fn get_sessions_page(
1128 1172 .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
1129 1173
1130 1174 let roles = UserService::get_user_roles(&db, user_model.id).await?;
1131 - let user = SimpleUserProfile::from_model(&user_model, roles.clone());
1175 + let user = UserProfileDTO::new(user_model, roles.clone());
1176 +
1177 + let locale_str = locale.as_str();
1132 1178
1133 1179 let current_token_id: Option<uuid::Uuid> = session.get("refresh_token_id").await.ok().flatten();
1134 1180
@@ -1158,15 +1204,28 @@ pub async fn get_sessions_page(
1158 1204 let layout = crate::templates::LayoutContext::new("Sessions", "");
1159 1205 use crate::templates::helpers::csrf_token_helper;
1160 1206 let csrf_token = csrf_token_helper(&session).await;
1161 - let nav_items = build_nav_items(&roles, "settings");
1207 +
1208 + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
1209 + let nav_items = build_nav_items_from_dto(&navigation, "settings");
1210 +
1211 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
1212 + .await?
1213 + .map(|s| s.value)
1214 + .unwrap_or_else(|| "localhost".to_string());
1215 + let app_description = AppSettingsService::get_setting(&db, "app_description")
1216 + .await?
1217 + .map(|s| s.value)
1218 + .unwrap_or_else(|| "Secure password manager".to_string());
1219 +
1220 + let page_title = i18n_service.translate("sessions.page_title", locale_str).await;
1162 1221
1163 1222 let template = SessionsTemplate {
1164 - page_title: "Sessions actives".to_string(),
1223 + page_title,
1165 1224 csrf_token,
1166 1225 app_name: layout.app_name.clone(),
1167 1226 app_version: layout.app_version.clone(),
1168 - app_domain: "localhost".to_string(),
1169 - app_description: "Secure password manager".to_string(),
1227 + app_domain,
1228 + app_description,
1170 1229 username: user.username.clone(),
1171 1230 email: user.email.clone(),
1172 1231 roles_text: user.roles.join(", "),
@@ -1175,36 +1234,36 @@ pub async fn get_sessions_page(
1175 1234 sessions,
1176 1235 nav_items,
1177 1236 has_pending_password_change: false,
1178 - user_menu_profile: "Profil".to_string(),
1179 - user_menu_logout: "Déconnexion".to_string(),
1180 - user_menu_settings: "Paramètres".to_string(),
1181 - title: "Sessions actives".to_string(),
1182 - subtitle: "Gérez vos sessions de connexion".to_string(),
1183 - current_session: "Session actuelle".to_string(),
1184 - current_badge: "Actuelle".to_string(),
1185 - unknown_device: "Appareil inconnu".to_string(),
1186 - ip_address: "Adresse IP".to_string(),
1187 - created_at: "Créée le".to_string(),
1188 - last_activity: "Dernière activité".to_string(),
1189 - session_id: "ID de session".to_string(),
1190 - revoke: "Révoquer".to_string(),
1191 - revoke_confirm_template: "Êtes-vous sûr de vouloir révoquer cette session ?".to_string(),
1192 - revoke_success: "Session révoquée avec succès".to_string(),
1193 - revoke_error: "Erreur lors de la révocation".to_string(),
1194 - no_sessions: "Aucune session active".to_string(),
1195 - no_sessions_desc: "Vous n'avez pas de sessions actives".to_string(),
1196 - danger_zone: "Zone de danger".to_string(),
1197 - danger_zone_desc: "Révoquer toutes les sessions vous déconnectera de tous vos appareils".to_string(),
1198 - revoke_all: "Révoquer toutes les sessions".to_string(),
1199 - revoke_all_confirm: "Êtes-vous sûr de vouloir révoquer toutes les sessions ?".to_string(),
1200 - revoke_all_success: "Toutes les sessions ont été révoquées".to_string(),
1201 - revoke_all_error: "Erreur lors de la révocation des sessions".to_string(),
1202 - about_title: "À propos des sessions".to_string(),
1203 - about_desc: "Les sessions représentent vos connexions actives sur différents appareils".to_string(),
1204 - back_to_settings: "Retour aux paramètres".to_string(),
1205 - breadcrumb_home: "Accueil".to_string(),
1206 - breadcrumb_settings: "Paramètres".to_string(),
1207 - breadcrumb_sessions: "Sessions".to_string(),
1237 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
1238 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1239 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1240 + title: i18n_service.translate("sessions.title", locale_str).await,
1241 + subtitle: i18n_service.translate("sessions.subtitle", locale_str).await,
1242 + current_session: i18n_service.translate("sessions.current_session", locale_str).await,
1243 + current_badge: i18n_service.translate("sessions.current_badge", locale_str).await,
1244 + unknown_device: i18n_service.translate("sessions.unknown_device", locale_str).await,
1245 + ip_address: i18n_service.translate("sessions.ip_address", locale_str).await,
1246 + created_at: i18n_service.translate("sessions.created_at", locale_str).await,
1247 + last_activity: i18n_service.translate("sessions.last_activity", locale_str).await,
1248 + session_id: i18n_service.translate("sessions.session_id", locale_str).await,
1249 + revoke: i18n_service.translate("sessions.revoke", locale_str).await,
1250 + revoke_confirm_template: i18n_service.translate("sessions.revoke_confirm", locale_str).await,
1251 + revoke_success: i18n_service.translate("sessions.revoke_success", locale_str).await,
1252 + revoke_error: i18n_service.translate("sessions.revoke_error", locale_str).await,
1253 + no_sessions: i18n_service.translate("sessions.no_sessions", locale_str).await,
1254 + no_sessions_desc: i18n_service.translate("sessions.no_sessions_desc", locale_str).await,
1255 + danger_zone: i18n_service.translate("sessions.danger_zone", locale_str).await,
1256 + danger_zone_desc: i18n_service.translate("sessions.danger_zone_desc", locale_str).await,
1257 + revoke_all: i18n_service.translate("sessions.revoke_all", locale_str).await,
1258 + revoke_all_confirm: i18n_service.translate("sessions.revoke_all_confirm", locale_str).await,
1259 + revoke_all_success: i18n_service.translate("sessions.revoke_all_success", locale_str).await,
1260 + revoke_all_error: i18n_service.translate("sessions.revoke_all_error", locale_str).await,
1261 + about_title: i18n_service.translate("sessions.about_title", locale_str).await,
1262 + about_desc: i18n_service.translate("sessions.about_desc", locale_str).await,
1263 + back_to_settings: i18n_service.translate("sessions.back_to_settings", locale_str).await,
1264 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1265 + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await,
1266 + breadcrumb_sessions: i18n_service.translate("breadcrumb.sessions", locale_str).await,
1208 1267 };
1209 1268
1210 1269 Ok(template)
@@ -1213,6 +1272,8 @@ pub async fn get_sessions_page(
1213 1272 /// Handler for GET /settings/preferences - Display preferences page (PROTECTED)
1214 1273 pub async fn get_preferences_page(
1215 1274 Extension(claims): Extension<Claims>,
1275 + Extension(i18n_service): Extension<Arc<I18nService>>,
1276 + Extension(locale): Extension<Locale>,
1216 1277 State(db): State<DatabaseConnection>,
1217 1278 session: Session,
1218 1279 ) -> Result<impl IntoResponse, AppError> {
@@ -1221,7 +1282,9 @@ pub async fn get_preferences_page(
1221 1282 .ok_or_else(|| AppError::NotFound("User not found".to_string()))?;
1222 1283
1223 1284 let roles = UserService::get_user_roles(&db, user_model.id).await?;
1224 - let user = SimpleUserProfile::from_model(&user_model, roles.clone());
1285 + let user = UserProfileDTO::new(user_model, roles.clone());
1286 +
1287 + let locale_str = locale.as_str();
1225 1288
1226 1289 use crate::services::user_preferences_service::UserPreferencesService;
1227 1290 let preferences = match UserPreferencesService::get_preferences(&db, claims.sub).await? {
@@ -1229,7 +1292,7 @@ pub async fn get_preferences_page(
1229 1292 None => UserPreferencesService::create_default_preferences(&db, claims.sub).await?,
1230 1293 };
1231 1294
1232 - let current_locale = preferences.language.clone();
1295 + let current_locale = locale_str.to_string();
1233 1296 let supported_languages: Vec<(String, String, bool)> = vec![
1234 1297 ("fr".to_string(), "Français".to_string(), current_locale == "fr"),
1235 1298 ("en".to_string(), "English".to_string(), current_locale == "en"),
@@ -1240,13 +1303,24 @@ pub async fn get_preferences_page(
1240 1303 let layout = crate::templates::LayoutContext::new("Préférences", "");
1241 1304 use crate::templates::helpers::csrf_token_helper;
1242 1305 let csrf_token = csrf_token_helper(&session).await;
1243 - let nav_items = build_nav_items(&roles, "settings");
1244 1306
1245 - let save_button = "Enregistrer".to_string();
1246 - let saved_success = "Préférences enregistrées avec succès".to_string();
1247 - let saved_reload = "Préférences enregistrées. Rechargement en cours...".to_string();
1248 - let error_prefix = "Erreur : ".to_string();
1249 - let network_error = "Erreur réseau".to_string();
1307 + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await;
1308 + let nav_items = build_nav_items_from_dto(&navigation, "settings");
1309 +
1310 + let app_domain = AppSettingsService::get_setting(&db, "app_domain")
1311 + .await?
1312 + .map(|s| s.value)
1313 + .unwrap_or_else(|| "localhost".to_string());
1314 + let app_description = AppSettingsService::get_setting(&db, "app_description")
1315 + .await?
1316 + .map(|s| s.value)
1317 + .unwrap_or_else(|| "Secure password manager".to_string());
1318 +
1319 + let save_button = i18n_service.translate("preferences.save", locale_str).await;
1320 + let saved_success = i18n_service.translate("preferences.saved_success", locale_str).await;
1321 + let saved_reload = i18n_service.translate("preferences.saved_reload", locale_str).await;
1322 + let error_prefix = i18n_service.translate("preferences.error", locale_str).await;
1323 + let network_error = i18n_service.translate("preferences.network_error", locale_str).await;
1250 1324
1251 1325 let saved_reload_json = serde_json::to_string(&saved_reload).unwrap_or_else(|_| "\"\"".to_string());
1252 1326 let saved_success_json = serde_json::to_string(&saved_success).unwrap_or_else(|_| "\"\"".to_string());
@@ -1259,8 +1333,8 @@ pub async fn get_preferences_page(
1259 1333 csrf_token,
1260 1334 app_name: layout.app_name.clone(),
1261 1335 app_version: layout.app_version.clone(),
1262 - app_domain: "localhost".to_string(),
1263 - app_description: "Secure password manager".to_string(),
1336 + app_domain,
1337 + app_description,
1264 1338 username: user.username.clone(),
1265 1339 email: user.email.clone(),
1266 1340 roles_text: user.roles.join(", "),
@@ -1279,34 +1353,34 @@ pub async fn get_preferences_page(
1279 1353 supported_languages,
1280 1354 nav_items,
1281 1355 has_pending_password_change: false,
1282 - user_menu_profile: "Profil".to_string(),
1283 - user_menu_logout: "Déconnexion".to_string(),
1284 - user_menu_settings: "Paramètres".to_string(),
1285 - title: "Préférences".to_string(),
1286 - subtitle: "Personnalisez votre expérience".to_string(),
1287 - theme_label: "Thème".to_string(),
1288 - theme_light: "Clair".to_string(),
1289 - theme_light_desc: "Interface lumineuse".to_string(),
1290 - theme_dark: "Sombre".to_string(),
1291 - theme_dark_desc: "Interface sombre".to_string(),
1292 - theme_auto: "Automatique".to_string(),
1293 - theme_auto_desc: "Suit les préférences système".to_string(),
1294 - theme_preview: "Aperçu du thème".to_string(),
1295 - language_label: "Langue".to_string(),
1296 - language_fr: "Français".to_string(),
1297 - language_fr_desc: "Interface en français".to_string(),
1298 - language_en: "English".to_string(),
1299 - language_en_desc: "Interface in English".to_string(),
1300 - language_de: "Deutsch".to_string(),
1301 - language_de_desc: "Benutzeroberfläche auf Deutsch".to_string(),
1302 - language_es: "Español".to_string(),
1303 - language_es_desc: "Interfaz en español".to_string(),
1304 - translation_warning: "Certaines traductions peuvent être incomplètes".to_string(),
1305 - modified: "Modifié".to_string(),
1306 - modified_desc: "Vous avez des modifications non enregistrées".to_string(),
1356 + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await,
1357 + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await,
1358 + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await,
1359 + title: i18n_service.translate("preferences.title", locale_str).await,
1360 + subtitle: i18n_service.translate("preferences.subtitle", locale_str).await,
1361 + theme_label: i18n_service.translate("preferences.theme", locale_str).await,
1362 + theme_light: i18n_service.translate("preferences.theme_light", locale_str).await,
1363 + theme_light_desc: i18n_service.translate("preferences.theme_light_desc", locale_str).await,
1364 + theme_dark: i18n_service.translate("preferences.theme_dark", locale_str).await,
1365 + theme_dark_desc: i18n_service.translate("preferences.theme_dark_desc", locale_str).await,
1366 + theme_auto: i18n_service.translate("preferences.theme_auto", locale_str).await,
1367 + theme_auto_desc: i18n_service.translate("preferences.theme_auto_desc", locale_str).await,
1368 + theme_preview: i18n_service.translate("preferences.theme_preview", locale_str).await,
1369 + language_label: i18n_service.translate("preferences.language", locale_str).await,
1370 + language_fr: i18n_service.translate("preferences.language_fr", locale_str).await,
1371 + language_fr_desc: i18n_service.translate("preferences.language_fr_desc", locale_str).await,
1372 + language_en: i18n_service.translate("preferences.language_en", locale_str).await,
1373 + language_en_desc: i18n_service.translate("preferences.language_en_desc", locale_str).await,
1374 + language_de: i18n_service.translate("preferences.language_de", locale_str).await,
1375 + language_de_desc: i18n_service.translate("preferences.language_de_desc", locale_str).await,
1376 + language_es: i18n_service.translate("preferences.language_es", locale_str).await,
1377 + language_es_desc: i18n_service.translate("preferences.language_es_desc", locale_str).await,
1378 + translation_warning: i18n_service.translate("preferences.translation_warning", locale_str).await,
1379 + modified: i18n_service.translate("preferences.modified", locale_str).await,
1380 + modified_desc: i18n_service.translate("preferences.modified_desc", locale_str).await,
1307 1381 save_button,
1308 - saving: "Enregistrement...".to_string(),
1309 - cancel: "Annuler".to_string(),
1382 + saving: i18n_service.translate("preferences.saving", locale_str).await,
1383 + cancel: i18n_service.translate("preferences.cancel", locale_str).await,
1310 1384 saved_success,
1311 1385 saved_reload,
1312 1386 saved_reload_json,
@@ -1316,14 +1390,14 @@ pub async fn get_preferences_page(
1316 1390 network_error,
1317 1391 network_error_json,
1318 1392 save_button_json,
1319 - sync_title: "Synchronisation".to_string(),
1320 - sync_desc: "Les préférences sont synchronisées sur tous vos appareils".to_string(),
1321 - preview_dark: "Aperçu sombre".to_string(),
1322 - preview_light: "Aperçu clair".to_string(),
1323 - preview_auto: "Aperçu automatique".to_string(),
1324 - breadcrumb_home: "Accueil".to_string(),
1325 - breadcrumb_settings: "Paramètres".to_string(),
1326 - breadcrumb_preferences: "Préférences".to_string(),
1393 + sync_title: i18n_service.translate("preferences.sync_title", locale_str).await,
1394 + sync_desc: i18n_service.translate("preferences.sync_desc", locale_str).await,
1395 + preview_dark: i18n_service.translate("preferences.preview_dark", locale_str).await,
1396 + preview_light: i18n_service.translate("preferences.preview_light", locale_str).await,
1397 + preview_auto: i18n_service.translate("preferences.preview_auto", locale_str).await,
1398 + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await,
1399 + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await,
1400 + breadcrumb_preferences: i18n_service.translate("breadcrumb.preferences", locale_str).await,
1327 1401 };
1328 1402
1329 1403 Ok(template)
M src/main.rs
+74 -0
@@ -1,10 +1,51 @@
1 +use axum::{extract::Request, middleware::Next, response::Response};
1 2 use rustwarden::auth::session::SessionConfig;
2 3 use rustwarden::config::auth::AuthConfig;
3 4 use rustwarden::config::database;
4 5 use rustwarden::config::email::EmailConfig;
5 6 use rustwarden::config::server::ServerConfig;
7 +use rustwarden::i18n::extractor::Locale;
6 8 use sea_orm::DatabaseConnection;
7 9
10 +/// Middleware that extracts the user's locale from the request and injects it as an Extension.
11 +/// Priority: cookie "locale" -> Accept-Language header -> default "fr"
12 +async fn locale_middleware(request: Request, next: Next) -> Response {
13 + let (mut parts, body) = request.into_parts();
14 +
15 + let cookie_jar = axum_extra::extract::CookieJar::from_headers(&parts.headers);
16 + let locale_str = cookie_jar
17 + .get("locale")
18 + .and_then(|c| {
19 + let v = c.value();
20 + if matches!(v, "fr" | "en" | "de" | "es") {
21 + Some(v.to_string())
22 + } else {
23 + None
24 + }
25 + })
26 + .or_else(|| {
27 + parts
28 + .headers
29 + .get("accept-language")
30 + .and_then(|h| h.to_str().ok())
31 + .and_then(|h| {
32 + h.split(',')
33 + .next()
34 + .and_then(|s| s.split(';').next())
35 + .map(|s| s.trim())
36 + .filter(|s| s.len() >= 2)
37 + .map(|s| s[..2].to_string())
38 + .filter(|s| matches!(s.as_str(), "fr" | "en" | "de" | "es"))
39 + })
40 + })
41 + .unwrap_or_else(|| "fr".to_string());
42 +
43 + parts.extensions.insert(Locale(locale_str));
44 +
45 + let request = Request::from_parts(parts, body);
46 + next.run(request).await
47 +}
48 +
8 49 #[tokio::main]
9 50 async fn main() {
10 51 dotenvy::dotenv().ok();
@@ -127,6 +168,37 @@ async fn main() {
127 168 });
128 169 tracing::info!("JWT blacklist cleanup task started (runs every hour)");
129 170
171 + // Initialize I18nService and load translations from YAML files
172 + let i18n_service = {
173 + use rustwarden::i18n::service::I18nService;
174 + use rustwarden::i18n::loader::load_translations_from_directory;
175 + use std::sync::Arc;
176 +
177 + let service = I18nService::new();
178 + match load_translations_from_directory("locales").await {
179 + Ok(all_translations) => {
180 + let mut locale_count = 0;
181 + let mut key_count = 0;
182 + for (locale, translations) in all_translations {
183 + key_count += translations.len();
184 + locale_count += 1;
185 + if let Err(e) = service.load_translations(locale.clone(), translations).await {
186 + tracing::error!("Failed to load translations for locale {}: {}", locale, e);
187 + }
188 + }
189 + tracing::info!(
190 + "I18n service initialized: {} locales, {} translation keys",
191 + locale_count,
192 + key_count
193 + );
194 + }
195 + Err(e) => {
196 + tracing::warn!("Failed to load translations: {}. I18n will use fallback keys.", e);
197 + }
198 + }
199 + Arc::new(service)
200 + };
201 +
130 202 let router = rustwarden::api::routes::create_router();
131 203
132 204 use rustwarden::api::middleware::admin_service::AdminService;
@@ -141,6 +213,8 @@ async fn main() {
141 213 .layer_fn(move |service| AdminService::new(service, db_for_admin.clone()))
142 214 .layer_fn(move |service| AuthService::new(service, db_for_auth.clone()))
143 215 .layer(session_layer)
216 + .layer(axum::middleware::from_fn(locale_middleware))
217 + .layer(axum::Extension(i18n_service))
144 218 .into_inner(),
145 219 );
146 220
M templates/admin/dashboard.html
+28 -29
@@ -7,23 +7,23 @@
7 7 <!-- Breadcrumb -->
8 8 <div class="text-sm breadcrumbs mb-3">
9 9 <ul>
10 - <li><a href="/">Accueil</a></li>
11 - <li>Administration</li>
10 + <li><a href="/">{{ breadcrumb_home }}</a></li>
11 + <li>{{ breadcrumb_admin }}</li>
12 12 </ul>
13 13 </div>
14 14
15 - <h1 class="text-2xl font-bold mb-4">Tableau de bord Administrateur</h1>
15 + <h1 class="text-2xl font-bold mb-4">{{ heading }}</h1>
16 16
17 17 <!-- Statistics Cards -->
18 18 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
19 19 <!-- Total Users Card -->
20 20 <div class="card bg-base-100 shadow-md">
21 21 <div class="card-body p-4">
22 - <h2 class="card-title text-primary text-base">👥 Utilisateurs</h2>
23 - <p class="text-xs text-base-content/70 mb-1">Total utilisateurs</p>
22 + <h2 class="card-title text-primary text-base">👥 {{ users_card_title }}</h2>
23 + <p class="text-xs text-base-content/70 mb-1">{{ users_card_subtitle }}</p>
24 24 <p class="text-2xl font-bold">{{ total_users }}</p>
25 25 <div class="card-actions justify-end mt-2">
26 - <a href="/admin/users" class="btn btn-primary btn-sm">Accéder</a>
26 + <a href="/admin/users" class="btn btn-primary btn-sm">{{ access_button }}</a>
27 27 </div>
28 28 </div>
29 29 </div>
@@ -31,8 +31,8 @@
31 31 <!-- Active Sessions Card -->
32 32 <div class="card bg-base-100 shadow-md">
33 33 <div class="card-body p-4">
34 - <h2 class="card-title text-secondary text-base">🔐 Sessions actives</h2>
35 - <p class="text-xs text-base-content/70 mb-1">Sessions en cours</p>
34 + <h2 class="card-title text-secondary text-base">🔐 {{ sessions_card_title }}</h2>
35 + <p class="text-xs text-base-content/70 mb-1">{{ sessions_card_subtitle }}</p>
36 36 <p class="text-2xl font-bold">{{ active_sessions }}</p>
37 37 </div>
38 38 </div>
@@ -40,8 +40,8 @@
40 40 <!-- Admins Card -->
41 41 <div class="card bg-base-100 shadow-md">
42 42 <div class="card-body p-4">
43 - <h2 class="card-title text-accent text-base">👑 Administrateurs</h2>
44 - <p class="text-xs text-base-content/70 mb-1">Comptes admin</p>
43 + <h2 class="card-title text-accent text-base">👑 {{ admins_card_title }}</h2>
44 + <p class="text-xs text-base-content/70 mb-1">{{ admins_card_subtitle }}</p>
45 45 <p class="text-2xl font-bold">{{ total_admins }}</p>
46 46 </div>
47 47 </div>
@@ -49,8 +49,8 @@
49 49 <!-- Last Activity Card -->
50 50 <div class="card bg-base-100 shadow-md">
51 51 <div class="card-body p-4">
52 - <h2 class="card-title text-info text-base">⏰ Dernière activité</h2>
53 - <p class="text-xs text-base-content/70 mb-1">Dernière connexion</p>
52 + <h2 class="card-title text-info text-base">⏰ {{ last_activity_card_title }}</h2>
53 + <p class="text-xs text-base-content/70 mb-1">{{ last_activity_card_subtitle }}</p>
54 54 <p class="text-base font-semibold">{{ last_activity }}</p>
55 55 </div>
56 56 </div>
@@ -60,7 +60,7 @@
60 60 {% if has_audit_logs %}
61 61 <div class="card bg-base-100 shadow-md mb-4">
62 62 <div class="card-body p-4">
63 - <h2 class="card-title text-lg mb-3">📊 Statistiques des logs d'audit</h2>
63 + <h2 class="card-title text-lg mb-3">📊 {{ audit_section_title }}</h2>
64 64 <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
65 65 <!-- Histogram Chart -->
66 66 <div>
@@ -70,16 +70,16 @@
70 70 <div class="space-y-3">
71 71 <div class="grid grid-cols-2 gap-3">
72 72 <div>
73 - <p class="text-xs text-base-content/70">Total des logs</p>
73 + <p class="text-xs text-base-content/70">{{ audit_total_logs_label }}</p>
74 74 <p class="text-xl font-bold">{{ audit_total_logs }}</p>
75 75 </div>
76 76 <div>
77 - <p class="text-xs text-base-content/70">Dernières 24h</p>
77 + <p class="text-xs text-base-content/70">{{ audit_last_24h }}</p>
78 78 <p class="text-xl font-bold">{{ audit_logs_last_24h }}</p>
79 79 </div>
80 80 </div>
81 81 <div>
82 - <p class="text-xs text-base-content/70 mb-2">Par niveau</p>
82 + <p class="text-xs text-base-content/70 mb-2">{{ audit_by_level }}</p>
83 83 <div class="flex flex-wrap gap-2">
84 84 <span class="badge badge-info badge-sm">DEBUG: {{ audit_debug_count }}</span>
85 85 <span class="badge badge-success badge-sm">INFO: {{ audit_info_count }}</span>
@@ -97,40 +97,40 @@
97 97 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
98 98 <a href="/admin/settings" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
99 99 <div class="card-body p-4">
100 - <h2 class="card-title text-secondary text-base">⚙️ Paramètres Application</h2>
101 - <p class="text-sm">Configurer les paramètres globaux de l'application</p>
100 + <h2 class="card-title text-secondary text-base">⚙️ {{ settings_card_title }}</h2>
101 + <p class="text-sm">{{ settings_card_desc }}</p>
102 102 <div class="card-actions justify-end mt-2">
103 - <button class="btn btn-secondary btn-sm">Accéder</button>
103 + <button class="btn btn-secondary btn-sm">{{ access_button }}</button>
104 104 </div>
105 105 </div>
106 106 </a>
107 107
108 108 <a href="/admin/apps" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
109 109 <div class="card-body p-4">
110 - <h2 class="card-title text-accent text-base">📦 Gestion des Applications</h2>
111 - <p class="text-sm">Activer ou désactiver les applications modulaires</p>
110 + <h2 class="card-title text-accent text-base">📦 {{ apps_card_title }}</h2>
111 + <p class="text-sm">{{ apps_card_desc }}</p>
112 112 <div class="card-actions justify-end mt-2">
113 - <button class="btn btn-accent btn-sm">Accéder</button>
113 + <button class="btn btn-accent btn-sm">{{ access_button }}</button>
114 114 </div>
115 115 </div>
116 116 </a>
117 117
118 118 <a href="/admin/organisations" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
119 119 <div class="card-body p-4">
120 - <h2 class="card-title text-info text-base">🏢 Gestion des Organisations</h2>
121 - <p class="text-sm">Créer et gérer les organisations et leurs membres</p>
120 + <h2 class="card-title text-info text-base">🏢 {{ orgs_card_title }}</h2>
121 + <p class="text-sm">{{ orgs_card_desc }}</p>
122 122 <div class="stats stats-vertical lg:stats-horizontal shadow-sm mt-2">
123 123 <div class="stat py-1 px-2">
124 - <div class="stat-title text-xs">Organisations</div>
124 + <div class="stat-title text-xs">{{ orgs_stat_title }}</div>
125 125 <div class="stat-value text-base">{{ total_organisations }}</div>
126 126 </div>
127 127 <div class="stat py-1 px-2">
128 - <div class="stat-title text-xs">Membres totaux</div>
128 + <div class="stat-title text-xs">{{ orgs_members_stat_title }}</div>
129 129 <div class="stat-value text-base">{{ total_organisation_members }}</div>
130 130 </div>
131 131 </div>
132 132 <div class="card-actions justify-end mt-2">
133 - <button class="btn btn-info btn-sm">Accéder</button>
133 + <button class="btn btn-info btn-sm">{{ access_button }}</button>
134 134 </div>
135 135 </div>
136 136 </a>
@@ -150,7 +150,7 @@ document.addEventListener('DOMContentLoaded', function() {
150 150 data: {
151 151 labels: ['DEBUG', 'INFO', 'WARN', 'ERROR'],
152 152 datasets: [{
153 - label: 'Logs par niveau',
153 + label: '{{ chart_label }}',
154 154 data: [
155 155 {{ audit_debug_count }},
156 156 {{ audit_info_count }},
@@ -192,4 +192,3 @@ document.addEventListener('DOMContentLoaded', function() {
192 192 </script>
193 193 {% endif %}
194 194 {% endblock %}
195 -
M templates/admin/errors.html
+18 -22
@@ -7,29 +7,26 @@
7 7 <!-- Breadcrumb -->
8 8 <div class="text-sm breadcrumbs mb-3">
9 9 <ul>
10 - <li><a href="/">Accueil</a></li>
11 - <li><a href="/admin">Administration</a></li>
12 - <li>Codes d'Erreur</li>
10 + <li><a href="/">{{ breadcrumb_home }}</a></li>
11 + <li><a href="/admin">{{ breadcrumb_admin }}</a></li>
12 + <li>{{ breadcrumb_errors }}</li>
13 13 </ul>
14 14 </div>
15 15
16 - <h1 class="text-2xl font-bold mb-4">Codes d'Erreur</h1>
17 - <p class="text-base-content/70 mb-6">
18 - Documentation des codes d'erreur de l'application. Chaque code d'erreur est associé à un middleware
19 - et fournit des informations de contexte pour le débogage.
20 - </p>
16 + <h1 class="text-2xl font-bold mb-4">{{ title }}</h1>
17 + <p class="text-base-content/70 mb-6">{{ description }}</p>
21 18
22 19 <!-- Error Codes Table -->
23 20 <div class="overflow-x-auto">
24 21 <table class="table table-zebra w-full">
25 22 <thead>
26 23 <tr>
27 - <th>Code</th>
28 - <th>Description</th>
29 - <th>Catégorie</th>
30 - <th>Middleware</th>
31 - <th>Contexte</th>
32 - <th>Exemple</th>
24 + <th>{{ table_code }}</th>
25 + <th>{{ table_description }}</th>
26 + <th>{{ table_category }}</th>
27 + <th>{{ table_middleware }}</th>
28 + <th>{{ table_context }}</th>
29 + <th>{{ table_example }}</th>
33 30 </tr>
34 31 </thead>
35 32 <tbody>
@@ -50,17 +47,16 @@
50 47 <!-- Legend -->
51 48 <div class="mt-6 card bg-base-200">
52 49 <div class="card-body">
53 - <h2 class="card-title text-lg">Légende</h2>
50 + <h2 class="card-title text-lg">{{ legend_title }}</h2>
54 51 <ul class="list-disc list-inside space-y-2 text-sm">
55 - <li><strong>Code</strong> : Identifiant unique de l'erreur (format: CATEGORY_NUMBER)</li>
56 - <li><strong>Description</strong> : Description en français de l'erreur</li>
57 - <li><strong>Catégorie</strong> : Type d'erreur (Authentification, CSRF, Rate Limiting, etc.)</li>
58 - <li><strong>Middleware</strong> : Middleware responsable de générer cette erreur</li>
59 - <li><strong>Contexte</strong> : Explication détaillée de quand cette erreur se produit</li>
60 - <li><strong>Exemple</strong> : Exemple concret de situation générant cette erreur</li>
52 + <li><strong>{{ table_code }}</strong> : {{ legend_code }}</li>
53 + <li><strong>{{ table_description }}</strong> : {{ legend_description }}</li>
54 + <li><strong>{{ table_category }}</strong> : {{ legend_category }}</li>
55 + <li><strong>{{ table_middleware }}</strong> : {{ legend_middleware }}</li>
56 + <li><strong>{{ table_context }}</strong> : {{ legend_context }}</li>
57 + <li><strong>{{ table_example }}</strong> : {{ legend_example }}</li>
61 58 </ul>
62 59 </div>
63 60 </div>
64 61 </div>
65 62 {% endblock %}
66 -
M templates/admin/settings.html
+34 -40
@@ -112,14 +112,14 @@
112 112 <div class="card-body">
113 113 <h2 class="card-title text-xl mb-4">
114 114 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
115 - Sécurité des Mots de Passe
115 + {{ password_security }}
116 116 </h2>
117 117
118 118 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
119 119 <!-- Min Length -->
120 120 <div class="form-control">
121 121 <label class="label">
122 - <span class="label-text font-semibold">Longueur minimale</span>
122 + <span class="label-text font-semibold">{{ password_min_length }}</span>
123 123 </label>
124 124 <input
125 125 type="number"
@@ -130,14 +130,14 @@
130 130 max="128"
131 131 >
132 132 <label class="label">
133 - <span class="label-text-alt">Entre 6 et 128 caractères</span>
133 + <span class="label-text-alt">{{ password_min_length_hint }}</span>
134 134 </label>
135 135 </div>
136 136
137 137 <!-- Complexity Requirements -->
138 138 <div class="space-y-2">
139 139 <label class="label">
140 - <span class="label-text font-semibold">Exigences de complexité</span>
140 + <span class="label-text font-semibold">{{ password_complexity }}</span>
141 141 </label>
142 142 <div class="form-control">
143 143 <label class="label cursor-pointer justify-start gap-3 py-1">
@@ -148,7 +148,7 @@
148 148 class="checkbox checkbox-sm"
149 149 {% if settings.password_require_uppercase == "true" %}checked{% endif %}
150 150 >
151 - <span class="label-text">Majuscule requise</span>
151 + <span class="label-text">{{ require_uppercase }}</span>
152 152 </label>
153 153 </div>
154 154 <div class="form-control">
@@ -160,7 +160,7 @@
160 160 class="checkbox checkbox-sm"
161 161 {% if settings.password_require_lowercase == "true" %}checked{% endif %}
162 162 >
163 - <span class="label-text">Minuscule requise</span>
163 + <span class="label-text">{{ require_lowercase }}</span>
164 164 </label>
165 165 </div>
166 166 <div class="form-control">
@@ -172,7 +172,7 @@
172 172 class="checkbox checkbox-sm"
173 173 {% if settings.password_require_digits == "true" %}checked{% endif %}
174 174 >
175 - <span class="label-text">Chiffre requis</span>
175 + <span class="label-text">{{ require_digits }}</span>
176 176 </label>
177 177 </div>
178 178 <div class="form-control">
@@ -184,7 +184,7 @@
184 184 class="checkbox checkbox-sm"
185 185 {% if settings.password_require_special == "true" %}checked{% endif %}
186 186 >
187 - <span class="label-text">Caractère spécial requis</span>
187 + <span class="label-text">{{ require_special }}</span>
188 188 </label>
189 189 </div>
190 190 </div>
@@ -201,8 +201,8 @@
201 201 {% if settings.password_expiration_enabled == "true" %}checked{% endif %}
202 202 >
203 203 <div>
204 - <span class="label-text font-semibold">Expiration des mots de passe</span>
205 - <span class="label-text-alt block">Forcer le changement périodique</span>
204 + <span class="label-text font-semibold">{{ password_expiration }}</span>
205 + <span class="label-text-alt block">{{ password_expiration_desc }}</span>
206 206 </div>
207 207 </label>
208 208 </div>
@@ -210,7 +210,7 @@
210 210 <div id="expirationSettings" class="space-y-3">
211 211 <div class="form-control">
212 212 <label class="label">
213 - <span class="label-text">Expiration après (jours)</span>
213 + <span class="label-text">{{ expiration_days }}</span>
214 214 </label>
215 215 <input
216 216 type="number"
@@ -231,13 +231,13 @@
231 231 class="checkbox checkbox-sm"
232 232 {% if settings.password_expiration_alert_enabled == "true" %}checked{% endif %}
233 233 >
234 - <span class="label-text">Alerte email avant expiration</span>
234 + <span class="label-text">{{ expiration_alert }}</span>
235 235 </label>
236 236 </div>
237 237
238 238 <div class="form-control">
239 239 <label class="label">
240 - <span class="label-text">Alerte (jours avant)</span>
240 + <span class="label-text">{{ expiration_alert_days }}</span>
241 241 </label>
242 242 <input
243 243 type="number"
@@ -258,33 +258,33 @@
258 258 <div class="card-body">
259 259 <h2 class="card-title text-xl mb-4">
260 260 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
261 - Journalisation des Audits
261 + {{ audit_logging }}
262 262 </h2>
263 263
264 264 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
265 265 <!-- Log Level -->
266 266 <div class="form-control">
267 267 <label class="label">
268 - <span class="label-text font-semibold">Niveau de log</span>
268 + <span class="label-text font-semibold">{{ log_level }}</span>
269 269 </label>
270 270 <select
271 271 name="audit_log_level"
272 272 class="select select-bordered w-full"
273 273 >
274 - <option value="DEBUG" {% if settings.audit_log_level == "DEBUG" %}selected{% endif %}>DEBUG - Tous les détails</option>
275 - <option value="INFO" {% if settings.audit_log_level == "INFO" %}selected{% endif %}>INFO - Informations normales</option>
276 - <option value="WARN" {% if settings.audit_log_level == "WARN" %}selected{% endif %}>WARN - Avertissements uniquement</option>
277 - <option value="ERROR" {% if settings.audit_log_level == "ERROR" %}selected{% endif %}>ERROR - Erreurs uniquement</option>
274 + <option value="DEBUG" {% if settings.audit_log_level == "DEBUG" %}selected{% endif %}>{{ log_level_debug }}</option>
275 + <option value="INFO" {% if settings.audit_log_level == "INFO" %}selected{% endif %}>{{ log_level_info }}</option>
276 + <option value="WARN" {% if settings.audit_log_level == "WARN" %}selected{% endif %}>{{ log_level_warn }}</option>
277 + <option value="ERROR" {% if settings.audit_log_level == "ERROR" %}selected{% endif %}>{{ log_level_error }}</option>
278 278 </select>
279 279 <label class="label">
280 - <span class="label-text-alt">Contrôle la verbosité des logs d'audit</span>
280 + <span class="label-text-alt">{{ log_level_hint }}</span>
281 281 </label>
282 282 </div>
283 283
284 284 <!-- Retention -->
285 285 <div class="form-control">
286 286 <label class="label">
287 - <span class="label-text font-semibold">Rétention des logs (jours)</span>
287 + <span class="label-text font-semibold">{{ log_retention }}</span>
288 288 </label>
289 289 <input
290 290 type="number"
@@ -295,14 +295,14 @@
295 295 max="365"
296 296 >
297 297 <label class="label">
298 - <span class="label-text-alt">Durée de conservation des logs (1-365 jours)</span>
298 + <span class="label-text-alt">{{ log_retention_hint }}</span>
299 299 </label>
300 300 </div>
301 301 </div>
302 302
303 303 <div class="alert alert-info mt-4">
304 304 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
305 - <span class="text-sm">Les logs d'audit enregistrent toutes les actions administratives (création, modification, suppression d'utilisateurs).</span>
305 + <span class="text-sm">{{ audit_info_message }}</span>
306 306 </div>
307 307 </div>
308 308 </div>
@@ -312,14 +312,14 @@
312 312 <div class="card-body">
313 313 <div class="flex justify-between items-center">
314 314 <div>
315 - <h3 class="font-semibold">Sauvegarder les modifications</h3>
316 - <p class="text-sm text-base-content/70">Les paramètres seront appliqués immédiatement</p>
315 + <h3 class="font-semibold">{{ save_changes }}</h3>
316 + <p class="text-sm text-base-content/70">{{ save_changes_desc }}</p>
317 317 </div>
318 318 <div class="flex gap-2">
319 - <a href="/admin" class="btn btn-ghost">Annuler</a>
319 + <a href="/admin" class="btn btn-ghost">{{ cancel }}</a>
320 320 <button type="submit" class="btn btn-primary">
321 321 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
322 - Enregistrer les paramètres
322 + {{ save_button }}
323 323 </button>
324 324 </div>
325 325 </div>
@@ -329,7 +329,6 @@
329 329 </div>
330 330
331 331 <script>
332 -// Toggle expiration settings visibility
333 332 const expirationCheckbox = document.getElementById('expirationEnabled');
334 333 const expirationSettings = document.getElementById('expirationSettings');
335 334
@@ -343,10 +342,9 @@ function toggleExpirationSettings() {
343 342
344 343 if (expirationCheckbox) {
345 344 expirationCheckbox.addEventListener('change', toggleExpirationSettings);
346 - toggleExpirationSettings(); // Initialize on load
345 + toggleExpirationSettings();
347 346 }
348 347
349 -// Form submission
350 348 document.getElementById('settingsForm').addEventListener('submit', async function(e) {
351 349 e.preventDefault();
352 350
@@ -354,7 +352,7 @@ document.getElementById('settingsForm').addEventListener('submit', async functio
354 352 const submitBtn = this.querySelector('button[type="submit"]');
355 353
356 354 submitBtn.disabled = true;
357 - submitBtn.innerHTML = '<span class="loading loading-spinner"></span> Enregistrement...';
355 + submitBtn.innerHTML = '<span class="loading loading-spinner"></span> {{ saving }}';
358 356
359 357 try {
360 358 const response = await fetch('/admin/settings', {
@@ -368,23 +366,19 @@ document.getElementById('settingsForm').addEventListener('submit', async functio
368 366 const result = await response.json();
369 367
370 368 if (response.ok) {
371 - alert('✅ Paramètres enregistrés avec succès !');
369 + alert('✅ {{ save_success }}');
372 370 window.location.reload();
373 371 } else {
374 - alert('❌ Erreur : ' + (result.error || result.message || 'Échec de l\'enregistrement'));
372 + alert('❌ {{ save_error_prefix }} : ' + (result.error || result.message || ''));
375 373 submitBtn.disabled = false;
376 - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les paramètres';
374 + submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> {{ save_button }}';
377 375 }
378 376 } catch (error) {
379 377 console.error('Error:', error);
380 - alert('❌ Erreur réseau. Veuillez réessayer.');
378 + alert('❌ {{ network_error }}');
381 379 submitBtn.disabled = false;
382 - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les paramètres';
380 + submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> {{ save_button }}';
383 381 }
384 382 });
385 383 </script>
386 384 {% endblock %}
387 -
388 -
389 -
390 -
M templates/admin/user_form.html
+21 -37
@@ -7,32 +7,20 @@
7 7 <!-- Breadcrumb -->
8 8 <div class="text-sm breadcrumbs mb-6">
9 9 <ul>
10 - <li><a href="/">Accueil</a></li>
11 - <li><a href="/admin">Administration</a></li>
12 - <li><a href="/admin/users">Utilisateurs</a></li>
13 - <li>
14 - {% if mode == "create" %}
15 - Créer
16 - {% else %}
17 - Modifier
18 - {% endif %}
19 - </li>
10 + <li><a href="/">{{ breadcrumb_home }}</a></li>
11 + <li><a href="/admin">{{ breadcrumb_admin }}</a></li>
12 + <li><a href="/admin/users">{{ breadcrumb_users }}</a></li>
13 + <li>{{ breadcrumb_mode }}</li>
20 14 </ul>
21 15 </div>
22 16
23 17 <div class="flex justify-between items-center mb-6">
24 - <h1 class="text-3xl font-bold">
25 - {% if mode == "create" %}
26 - Créer un utilisateur
27 - {% else %}
28 - Modifier un utilisateur
29 - {% endif %}
30 - </h1>
18 + <h1 class="text-3xl font-bold">{{ form_title }}</h1>
31 19 <a href="/admin/users" class="btn btn-ghost">
32 20 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
33 21 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
34 22 </svg>
35 - Retour à la liste
23 + {{ back_to_list }}
36 24 </a>
37 25 </div>
38 26
@@ -68,7 +56,7 @@
68 56 <!-- Username Field -->
69 57 <div class="form-control">
70 58 <label class="label">
71 - <span class="label-text font-semibold">Nom d'utilisateur</span>
59 + <span class="label-text font-semibold">{{ username_label }}</span>
72 60 <span class="label-text-alt text-error">*</span>
73 61 </label>
74 62 {% if mode == "create" %}
@@ -76,7 +64,7 @@
76 64 type="text"
77 65 name="username"
78 66 value="{{ form_username }}"
79 - placeholder="nom_utilisateur"
67 + placeholder="{{ username_placeholder }}"
80 68 class="input input-bordered w-full"
81 69 required
82 70 >
@@ -90,21 +78,21 @@
90 78 <input type="hidden" name="username" value="{{ form_username }}">
91 79 {% endif %}
92 80 <label class="label">
93 - <span class="label-text-alt">Le nom d'utilisateur ne peut pas être modifié après création</span>
81 + <span class="label-text-alt">{{ username_readonly_hint }}</span>
94 82 </label>
95 83 </div>
96 84
97 85 <!-- Email Field -->
98 86 <div class="form-control">
99 87 <label class="label">
100 - <span class="label-text font-semibold">Email</span>
88 + <span class="label-text font-semibold">{{ email_label }}</span>
101 89 <span class="label-text-alt text-error">*</span>
102 90 </label>
103 91 <input
104 92 type="email"
105 93 name="email"
106 94 value="{{ form_email }}"
107 - placeholder="utilisateur@example.com"
95 + placeholder="{{ email_placeholder }}"
108 96 class="input input-bordered w-full"
109 97 required
110 98 >
@@ -114,17 +102,17 @@
114 102 {% if mode == "create" %}
115 103 <div class="form-control">
116 104 <label class="label">
117 - <span class="label-text font-semibold">Mot de passe</span>
118 - <span class="label-text-alt">Optionnel</span>
105 + <span class="label-text font-semibold">{{ password_label }}</span>
106 + <span class="label-text-alt">{{ password_optional }}</span>
119 107 </label>
120 108 <input
121 109 type="password"
122 110 name="password"
123 - placeholder="Laisser vide pour générer un mot de passe aléatoire"
111 + placeholder="{{ password_placeholder }}"
124 112 class="input input-bordered w-full"
125 113 >
126 114 <label class="label">
127 - <span class="label-text-alt">Si laissé vide, un mot de passe sécurisé sera généré automatiquement</span>
115 + <span class="label-text-alt">{{ password_hint }}</span>
128 116 </label>
129 117 </div>
130 118 {% endif %}
@@ -132,7 +120,7 @@
132 120 <!-- Roles Field -->
133 121 <div class="form-control">
134 122 <label class="label">
135 - <span class="label-text font-semibold">Rôles</span>
123 + <span class="label-text font-semibold">{{ roles_label }}</span>
136 124 <span class="label-text-alt text-error">*</span>
137 125 </label>
138 126 <div class="space-y-2">
@@ -155,7 +143,7 @@
155 143 {% endfor %}
156 144 </div>
157 145 <label class="label">
158 - <span class="label-text-alt">Sélectionnez au moins un rôle</span>
146 + <span class="label-text-alt">{{ roles_hint }}</span>
159 147 </label>
160 148 </div>
161 149
@@ -171,8 +159,8 @@
171 159 {% if must_change_password %}checked{% endif %}
172 160 >
173 161 <div class="flex-1">
174 - <span class="label-text font-medium">Forcer le changement de mot de passe</span>
175 - <p class="text-sm text-base-content/70">L'utilisateur devra changer son mot de passe à la prochaine connexion</p>
162 + <span class="label-text font-medium">{{ must_change_password_label }}</span>
163 + <p class="text-sm text-base-content/70">{{ must_change_password_desc }}</p>
176 164 </div>
177 165 </label>
178 166 </div>
@@ -180,14 +168,10 @@
180 168
181 169 <!-- Form Actions -->
182 170 <div class="flex justify-end gap-4 pt-4 border-t">
183 - <a href="/admin/users" class="btn btn-ghost">Annuler</a>
171 + <a href="/admin/users" class="btn btn-ghost">{{ cancel }}</a>
184 172 <button type="submit" class="btn btn-primary" id="submit-btn">
185 173 <span id="form-loading" class="htmx-indicator loading loading-spinner loading-sm mr-2"></span>
186 - {% if mode == "create" %}
187 - Créer l'utilisateur
188 - {% else %}
189 - Enregistrer les modifications
190 - {% endif %}
174 + {{ submit_label }}
191 175 </button>
192 176 </div>
193 177 </form>
M templates/admin/users_list.html
+11 -11
@@ -355,7 +355,7 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
355 355 const submitBtn = this.querySelector('button[type="submit"]');
356 356 const originalBtnText = submitBtn.innerHTML;
357 357 submitBtn.disabled = true;
358 - submitBtn.innerHTML = '<span class="loading loading-spinner"></span> Réinitialisation...';
358 + submitBtn.innerHTML = '<span class="loading loading-spinner"></span> {{ resetting }}';
359 359
360 360 try {
361 361 let response;
@@ -399,11 +399,11 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
399 399 successModal.className = 'modal modal-open';
400 400 successModal.innerHTML = `
401 401 <div class="modal-box">
402 - <h3 class="font-bold text-lg mb-4">Réinitialisation par email</h3>
402 + <h3 class="font-bold text-lg mb-4">{{ reset_by_email_title }}</h3>
403 403 <div class="mb-4">${message.innerHTML}</div>
404 404 <div class="modal-action">
405 405 <button type="button" class="btn btn-primary" onclick="this.closest('.modal').remove(); document.getElementById('resetPasswordModal').close();">
406 - Fermer
406 + {{ close }}
407 407 </button>
408 408 </div>
409 409 </div>
@@ -427,16 +427,16 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
427 427 passwordDisplay.className = 'modal modal-open';
428 428 passwordDisplay.innerHTML = `
429 429 <div class="modal-box">
430 - <h3 class="font-bold text-lg mb-4">✅ Mot de passe réinitialisé</h3>
430 + <h3 class="font-bold text-lg mb-4">✅ {{ password_reset_success_title }}</h3>
431 431 <div class="alert alert-warning mb-4">
432 432 <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
433 433 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
434 434 </svg>
435 - <span>⚠️ Ce mot de passe ne sera affiché qu'une seule fois. Copiez-le maintenant !</span>
435 + <span>⚠️ {{ password_warning_once }}</span>
436 436 </div>
437 437 <div class="form-control mb-4">
438 438 <label class="label">
439 - <span class="label-text font-semibold">Nouveau mot de passe</span>
439 + <span class="label-text font-semibold">{{ new_password_label }}</span>
440 440 </label>
441 441 <div class="input-group">
442 442 <input
@@ -454,13 +454,13 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
454 454 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
455 455 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
456 456 </svg>
457 - Copier
457 + {{ copy }}
458 458 </button>
459 459 </div>
460 460 </div>
461 461 <div class="modal-action">
462 462 <button type="button" class="btn btn-primary" onclick="this.closest('.modal').remove(); document.getElementById('resetPasswordModal').close(); window.location.reload();">
463 - Fermer
463 + {{ close }}
464 464 </button>
465 465 </div>
466 466 </div>
@@ -472,18 +472,18 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
472 472 const passwordInput = document.getElementById('displayPassword');
473 473 passwordInput.select();
474 474 document.execCommand('copy');
475 - alert('✅ Mot de passe copié dans le presse-papiers');
475 + alert('✅ {{ password_copied }}');
476 476 };
477 477 } else {
478 478 // No password to display (custom password was set)
479 - alert('✅ Mot de passe réinitialisé avec succès');
479 + alert('✅ {{ password_set_success }}');
480 480 document.getElementById('resetPasswordModal').close();
481 481 window.location.reload();
482 482 }
483 483 } else {
484 484 const errorText = await response.text();
485 485 console.error('Error:', errorText);
486 - let errorMessage = '❌ Erreur lors de la réinitialisation du mot de passe';
486 + let errorMessage = '❌ {{ reset_error }}';
487 487 try {
488 488 const errorJson = JSON.parse(errorText);
489 489 if (errorJson.message) {
M templates/base.html
+1 -1
@@ -3,7 +3,7 @@
3 3 <head>
4 4 <meta charset="UTF-8">
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 - <meta name="description" content="{% if app_description != "" %}{{ app_description }}{% else %}RustWarden - Cloud storage solution written in Rust{% endif %}">
6 + <meta name="description" content="{% if app_description != "" %}{{ app_description }}{% else %}RustWarden - web boilerplate solution written in Rust{% endif %}">
7 7 <title>{% block title %}{{ page_title }} - {{ app_name }}{% endblock %}</title>
8 8
9 9 <!-- Favicon -->
M templates/home.html
+7 -15
@@ -8,12 +8,12 @@
8 8 <!-- Welcome Section -->
9 9 <div class="mb-8">
10 10 <h1 class="text-3xl font-bold text-base-content mb-2">{{ welcome_message }}</h1>
11 - <p class="text-base-content/70">Bienvenue sur votre espace cloud personnel</p>
11 + <p class="text-base-content/70">{{ home_subtitle }}</p>
12 12 </div>
13 13
14 14 <!-- Account Stats -->
15 15 <div class="mb-12">
16 - <h2 class="text-xl font-semibold text-base-content mb-4">Mon Compte</h2>
16 + <h2 class="text-xl font-semibold text-base-content mb-4">{{ home_my_account }}</h2>
17 17 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
18 18 {% for stat in account_stats %}
19 19 <div class="card bg-base-100 shadow-md hover:shadow-xl transition-shadow">
@@ -37,7 +37,7 @@
37 37
38 38 <!-- Upcoming Features -->
39 39 <div>
40 - <h2 class="text-xl font-semibold text-base-content mb-4">Fonctionnalités à Venir</h2>
40 + <h2 class="text-xl font-semibold text-base-content mb-4">{{ home_upcoming_features }}</h2>
41 41 <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
42 42 {% for feature in upcoming_features %}
43 43 <div class="card bg-base-100 shadow-md hover:shadow-xl transition-shadow">
@@ -56,12 +56,7 @@
56 56 </div>
57 57 <div>
58 58 <h3 class="card-title text-lg">{{ feature.name }}</h3>
59 - <span class="badge
60 - {% if feature.badge == "Bientôt" %}
61 - badge-warning
62 - {% else %}
63 - badge-info
64 - {% endif %}">
59 + <span class="badge badge-warning">
65 60 {{ feature.badge }}
66 61 </span>
67 62 </div>
@@ -79,14 +74,11 @@
79 74 </div>
80 75 </div>
81 76
82 -<!-- Quick Actions (Optional) -->
77 +<!-- Quick Actions -->
83 78 <div class="mt-12 card bg-info/10 border border-info/20">
84 79 <div class="card-body p-6">
85 - <h3 class="card-title text-lg mb-2">💡 Le Saviez-Vous ?</h3>
86 - <p class="text-base-content/80">
87 - RustWarden est un fork de Nextcloud écrit en Rust pour offrir de meilleures performances
88 - et une sécurité renforcée. Restez à l'écoute pour les nouvelles fonctionnalités !
89 - </p>
80 + <h3 class="card-title text-lg mb-2">💡 {{ home_did_you_know }}</h3>
81 + <p class="text-base-content/80">{{ home_did_you_know_text }}</p>
90 82 </div>
91 83 </div>
92 84 {% endblock %}
M templates/resend_verification.html
+18 -14
@@ -1,14 +1,14 @@
1 1 {% extends "base.html" %}
2 2
3 -{% block title %}Renvoyer l'email de validation - RustWarden{% endblock %}
3 +{% block title %}{{ page_title }}{% endblock %}
4 4
5 5 {% block content %}
6 6 <div class="min-h-screen flex items-center justify-center bg-base-200 px-4">
7 7 <div class="max-w-lg w-full space-y-6">
8 8 <div class="text-center">
9 - <h1 class="text-3xl font-bold text-base-content mb-2">Renvoyer l'email de validation</h1>
9 + <h1 class="text-3xl font-bold text-base-content mb-2">{{ title }}</h1>
10 10 <p class="text-base-content/70">
11 - Entrez votre adresse email pour recevoir un nouveau lien de validation.
11 + {{ subtitle }}
12 12 </p>
13 13 </div>
14 14
@@ -26,7 +26,7 @@
26 26 <form id="resend-form" class="space-y-4">
27 27 <div class="form-control">
28 28 <label for="email" class="label">
29 - <span class="label-text">Adresse email</span>
29 + <span class="label-text">{{ email_label }}</span>
30 30 </label>
31 31 <input
32 32 type="email"
@@ -35,24 +35,29 @@
35 35 required
36 36 autocomplete="email"
37 37 class="input input-bordered w-full"
38 - placeholder="vous@example.com">
38 + placeholder="{{ email_placeholder }}">
39 39 </div>
40 40
41 41 <div class="form-control mt-6">
42 42 <button type="submit" class="btn btn-primary w-full">
43 - Envoyer le lien de validation
43 + {{ submit }}
44 44 </button>
45 45 </div>
46 46 </form>
47 47
48 48 <div class="mt-4 text-sm text-base-content/70 text-center space-y-2">
49 - <a href="/login" class="text-primary hover:underline">Revenir à la connexion</a>
49 + <a href="/login" class="text-primary hover:underline">{{ back_to_login }}</a>
50 50 </div>
51 51 </div>
52 52 </div>
53 53 </div>
54 54
55 55 <script>
56 + const MSG_ERROR_DEFAULT = "{{ error_default }}";
57 + const MSG_SUCCESS_DEFAULT = "{{ success_default }}";
58 + const MSG_ERROR_MISSING_EMAIL = "{{ error_missing_email }}";
59 + const MSG_ERROR_CANNOT_SEND = "{{ error_cannot_send }}";
60 +
56 61 const form = document.getElementById('resend-form');
57 62 const alertError = document.getElementById('alert-error');
58 63 const alertErrorText = document.getElementById('alert-error-text');
@@ -61,7 +66,7 @@
61 66
62 67 function showError(msg) {
63 68 if (alertError && alertErrorText) {
64 - alertErrorText.textContent = msg || "Impossible d'envoyer la demande.";
69 + alertErrorText.textContent = msg || MSG_ERROR_DEFAULT;
65 70 alertError.classList.remove('hidden');
66 71 }
67 72 if (alertSuccess) alertSuccess.classList.add('hidden');
@@ -69,7 +74,7 @@
69 74
70 75 function showSuccess(msg) {
71 76 if (alertSuccess && alertSuccessText) {
72 - alertSuccessText.textContent = msg || "Si le compte existe et n'est pas vérifié, un email a été envoyé.";
77 + alertSuccessText.textContent = msg || MSG_SUCCESS_DEFAULT;
73 78 alertSuccess.classList.remove('hidden');
74 79 }
75 80 if (alertError) alertError.classList.add('hidden');
@@ -79,7 +84,7 @@
79 84 e.preventDefault();
80 85 const email = document.getElementById('email')?.value;
81 86 if (!email) {
82 - showError("Veuillez renseigner votre email.");
87 + showError(MSG_ERROR_MISSING_EMAIL);
83 88 return;
84 89 }
85 90 fetch('/api/v1/auth/resend-verification', {
@@ -95,13 +100,12 @@
95 100 msg = data.message || data.error || msg;
96 101 } catch (_) {}
97 102 if (resp.ok) {
98 - showSuccess(msg || "Si le compte existe et n'est pas vérifié, un email a été envoyé.");
103 + showSuccess(msg || MSG_SUCCESS_DEFAULT);
99 104 } else {
100 - showError(msg || "Impossible d'envoyer la demande pour l'instant.");
105 + showError(msg || MSG_ERROR_CANNOT_SEND);
101 106 }
102 107 })
103 - .catch(() => showError("Impossible d'envoyer la demande pour l'instant."));
108 + .catch(() => showError(MSG_ERROR_CANNOT_SEND));
104 109 });
105 110 </script>
106 111 {% endblock %}
107 -
M templates/reset_password.html
+22 -15
@@ -11,7 +11,7 @@
11 11 </div>
12 12 <h1 class="text-3xl font-bold text-base-content mb-2">{{ reset_password_title }}</h1>
13 13 <p class="text-base-content/70">
14 - Entrez votre nouveau mot de passe ci-dessous.
14 + {{ subtitle }}
15 15 </p>
16 16 </div>
17 17
@@ -48,7 +48,7 @@
48 48
49 49 <div class="form-control">
50 50 <label for="confirm_password" class="label">
51 - <span class="label-text">Confirmer le mot de passe</span>
51 + <span class="label-text">{{ confirm_password_label }}</span>
52 52 </label>
53 53 <input
54 54 type="password"
@@ -57,7 +57,7 @@
57 57 required
58 58 autocomplete="new-password"
59 59 class="input input-bordered w-full"
60 - placeholder="Confirmez votre nouveau mot de passe">
60 + placeholder="{{ confirm_password_placeholder }}">
61 61 </div>
62 62
63 63 <div class="form-control mt-6">
@@ -75,14 +75,23 @@
75 75 </div>
76 76
77 77 <script>
78 - // Extract token from URL query parameter
78 + const MSG_MISSING_TOKEN_URL = "{{ missing_token_url }}";
79 + const MSG_MISSING_TOKEN = "{{ missing_token }}";
80 + const MSG_MISSING_NEW_PASSWORD = "{{ missing_new_password }}";
81 + const MSG_PASSWORDS_NOT_MATCH = "{{ passwords_not_match }}";
82 + const MSG_RESET_SUCCESS = "{{ reset_success_message }}";
83 + const MSG_ERROR_LINK_INVALID = "{{ error_link_invalid }}";
84 + const MSG_ERROR_GENERIC = "{{ error_generic }}";
85 + const MSG_ERROR_DEFAULT = "{{ error_message }}";
86 + const MSG_SUCCESS_DEFAULT = "{{ success_message }}";
87 +
79 88 const urlParams = new URLSearchParams(window.location.search);
80 89 const token = urlParams.get('token');
81 90
82 91 if (token) {
83 92 document.getElementById('token').value = token;
84 93 } else {
85 - showError("Token de réinitialisation manquant dans l'URL.");
94 + showError(MSG_MISSING_TOKEN_URL);
86 95 }
87 96
88 97 const form = document.getElementById('reset-form');
@@ -93,7 +102,7 @@
93 102
94 103 function showError(msg) {
95 104 if (alertError && alertErrorText) {
96 - alertErrorText.textContent = msg || "{{ error_message }}";
105 + alertErrorText.textContent = msg || MSG_ERROR_DEFAULT;
97 106 alertError.classList.remove('hidden');
98 107 }
99 108 if (alertSuccess) alertSuccess.classList.add('hidden');
@@ -101,7 +110,7 @@
101 110
102 111 function showSuccess(msg) {
103 112 if (alertSuccess && alertSuccessText) {
104 - alertSuccessText.textContent = msg || "{{ success_message }}";
113 + alertSuccessText.textContent = msg || MSG_SUCCESS_DEFAULT;
105 114 alertSuccess.classList.remove('hidden');
106 115 }
107 116 if (alertError) alertError.classList.add('hidden');
@@ -114,17 +123,17 @@
114 123 const confirmPassword = document.getElementById('confirm_password')?.value;
115 124
116 125 if (!token) {
117 - showError("Token de réinitialisation manquant.");
126 + showError(MSG_MISSING_TOKEN);
118 127 return;
119 128 }
120 129
121 130 if (!newPassword) {
122 - showError("Veuillez entrer un nouveau mot de passe.");
131 + showError(MSG_MISSING_NEW_PASSWORD);
123 132 return;
124 133 }
125 134
126 135 if (newPassword !== confirmPassword) {
127 - showError("Les mots de passe ne correspondent pas.");
136 + showError(MSG_PASSWORDS_NOT_MATCH);
128 137 return;
129 138 }
130 139
@@ -141,17 +150,15 @@
141 150 msg = data.message || data.error || msg;
142 151 } catch (_) {}
143 152 if (resp.ok) {
144 - showSuccess(msg || "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter.");
145 - // Redirect to login after 3 seconds
153 + showSuccess(msg || MSG_RESET_SUCCESS);
146 154 setTimeout(() => {
147 155 window.location.href = '/login';
148 156 }, 3000);
149 157 } else {
150 - showError(msg || "Impossible de réinitialiser le mot de passe. Le lien peut être expiré ou invalide.");
158 + showError(msg || MSG_ERROR_LINK_INVALID);
151 159 }
152 160 })
153 - .catch(() => showError("Impossible de réinitialiser le mot de passe pour l'instant."));
161 + .catch(() => showError(MSG_ERROR_GENERIC));
154 162 });
155 163 </script>
156 164 {% endblock %}
157 -
M templates/settings.html
+19 -22
@@ -1,6 +1,6 @@
1 1 {% extends "base.html" %}
2 2
3 -{% block title %}Paramètres - RustWarden{% endblock %}
3 +{% block title %}{{ page_title }}{% endblock %}
4 4
5 5 {% block navbar %}
6 6 {% include "components/navbar.html" %}
@@ -11,15 +11,15 @@
11 11 <!-- Breadcrumb -->
12 12 <div class="text-sm breadcrumbs mb-6">
13 13 <ul>
14 - <li><a href="/">Accueil</a></li>
15 - <li>Paramètres</li>
14 + <li><a href="/">{{ breadcrumb_home }}</a></li>
15 + <li>{{ breadcrumb_settings }}</li>
16 16 </ul>
17 17 </div>
18 18
19 19 <!-- Page Header -->
20 20 <div class="mb-8">
21 - <h1 class="text-3xl font-bold mb-2">⚙️ Paramètres</h1>
22 - <p class="text-base-content/70">Gérez vos préférences et paramètres de compte</p>
21 + <h1 class="text-3xl font-bold mb-2">⚙️ {{ settings_title }}</h1>
22 + <p class="text-base-content/70">{{ settings_subtitle }}</p>
23 23 </div>
24 24
25 25 <!-- Settings Navigation Cards -->
@@ -31,11 +31,11 @@
31 31 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
32 32 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
33 33 </svg>
34 - Mon Profil
34 + {{ profile_title }}
35 35 </h2>
36 - <p class="text-sm text-base-content/70">Modifiez vos informations personnelles (nom d'utilisateur, email)</p>
36 + <p class="text-sm text-base-content/70">{{ profile_desc }}</p>
37 37 <div class="card-actions justify-end">
38 - <button class="btn btn-primary btn-sm">Accéder</button>
38 + <button class="btn btn-primary btn-sm">{{ access_button }}</button>
39 39 </div>
40 40 </div>
41 41 </a>
@@ -47,11 +47,11 @@
47 47 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
48 48 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
49 49 </svg>
50 - Mot de passe
50 + {{ password_title }}
51 51 </h2>
52 - <p class="text-sm text-base-content/70">Changez votre mot de passe pour sécuriser votre compte</p>
52 + <p class="text-sm text-base-content/70">{{ password_desc }}</p>
53 53 <div class="card-actions justify-end">
54 - <button class="btn btn-secondary btn-sm">Accéder</button>
54 + <button class="btn btn-secondary btn-sm">{{ access_button }}</button>
55 55 </div>
56 56 </div>
57 57 </a>
@@ -63,11 +63,11 @@
63 63 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
64 64 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
65 65 </svg>
66 - Sessions actives
66 + {{ sessions_title }}
67 67 </h2>
68 - <p class="text-sm text-base-content/70">Gérez vos sessions actives et révoquez l'accès depuis d'autres appareils</p>
68 + <p class="text-sm text-base-content/70">{{ sessions_desc }}</p>
69 69 <div class="card-actions justify-end">
70 - <button class="btn btn-accent btn-sm">Accéder</button>
70 + <button class="btn btn-accent btn-sm">{{ access_button }}</button>
71 71 </div>
72 72 </div>
73 73 </a>
@@ -79,11 +79,11 @@
79 79 <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
80 80 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
81 81 </svg>
82 - Préférences
82 + {{ preferences_title }}
83 83 </h2>
84 - <p class="text-sm text-base-content/70">Personnalisez votre expérience (thème, langue)</p>
84 + <p class="text-sm text-base-content/70">{{ preferences_desc }}</p>
85 85 <div class="card-actions justify-end">
86 - <button class="btn btn-info btn-sm">Accéder</button>
86 + <button class="btn btn-info btn-sm">{{ access_button }}</button>
87 87 </div>
88 88 </div>
89 89 </a>
@@ -92,11 +92,8 @@
92 92 <!-- Quick Info -->
93 93 <div class="mt-8 card bg-base-200 shadow-xl">
94 94 <div class="card-body">
95 - <h3 class="card-title text-lg">ℹ️ Informations</h3>
96 - <p class="text-sm text-base-content/70">
97 - Utilisez les sections ci-dessus pour gérer différents aspects de votre compte.
98 - Toutes les modifications sont sécurisées et nécessitent votre authentification.
99 - </p>
95 + <h3 class="card-title text-lg">ℹ️ {{ info_title }}</h3>
96 + <p class="text-sm text-base-content/70">{{ info_desc }}</p>
100 97 </div>
101 98 </div>
102 99 </div>