From 1f829f03821ee1ff9b03413c05fbf2a36b61a085 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Fri, 11 Apr 2025 11:44:40 +0200 Subject: [PATCH 1/9] Create profile preferences table --- ...20250411081424_create_profile_preferences_table.rb | 11 +++++++++++ db/schema.rb | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 db/migrations/20250411081424_create_profile_preferences_table.rb diff --git a/db/migrations/20250411081424_create_profile_preferences_table.rb b/db/migrations/20250411081424_create_profile_preferences_table.rb new file mode 100644 index 0000000..5f4d749 --- /dev/null +++ b/db/migrations/20250411081424_create_profile_preferences_table.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +Sequel.migration do + change do + create_table(:profile_preferences) do + primary_key :id, type: :Bignum + foreign_key :profile_id, :profiles, type: "uuid", null: false, key: %i[id] + column :preferences, "jsonb", null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fa4560e..a466b08 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,5 @@ # **Autogenerated file! Do not modify by hand!** -# Current migration: 20250214104251_add_pronouns_field.rb +# Current migration: 20250411081424_create_profile_preferences_table.rb Sequel.migration do change do @@ -416,6 +416,12 @@ index %i[profile_id target_profile_id], unique: true end + create_table(:profile_preferences) do + primary_key :id, type: :Bignum + foreign_key :profile_id, :profiles, type: "uuid", null: false, key: %i[id] + column :preferences, "jsonb", null: false + end + create_table(:reports) do primary_key :id, type: :Bignum column :created_at, "timestamp with time zone", default: Sequel::CURRENT_TIMESTAMP, null: false From 3c7975794330b2bbd26c79dd101be01f699a4c50 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Fri, 11 Apr 2025 11:45:37 +0200 Subject: [PATCH 2/9] Add Rack::Locale middleware to set I18n.locale --- Gemfile | 1 + Gemfile.lock | 4 ++++ app/api/rodauth_middleware.rb | 4 +++- config.ru | 1 + test/rack_helper.rb | 4 ++-- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bc70ae9..883c96c 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem "grape-swagger-entity" gem "falcon" # The app server gem "i18n" # Used for translating strings in the views +gem "rack-contrib" # Used for parsing the user's language into a locale gem "bcrypt" # Used by rodauth for password hashing gem "jwt" # Used by rodauth jwt diff --git a/Gemfile.lock b/Gemfile.lock index f821feb..2e7255d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -327,6 +327,8 @@ GEM public_suffix (6.0.2) racc (1.8.1) rack (3.1.16) + rack-contrib (2.5.0) + rack (< 4) rack-test (2.2.0) rack (>= 1.3) rainbow (3.1.1) @@ -480,6 +482,7 @@ DEPENDENCIES pg pronto (~> 0.11) pronto-rubocop + rack-contrib rack-test rake rerun @@ -630,6 +633,7 @@ CHECKSUMS public_suffix (6.0.2) sha256=bfa7cd5108066f8c9602e0d6d4114999a5df5839a63149d3e8b0f9c1d3558394 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rack (3.1.16) sha256=efb5606c351efc56b85b10c3493055d0d35209d23f44792ec4e1183eb0234635 + rack-contrib (2.5.0) sha256=51bd2ce82b8a3270f9173130c4cdaea72aab8b03dce8cd6af1c4344d84a32d02 rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.3.0) sha256=96f5092d786ff412c62fde76f793cc0541bd84d2eb579caa529aa8a059934493 diff --git a/app/api/rodauth_middleware.rb b/app/api/rodauth_middleware.rb index d5f7fe2..2ea6d6c 100644 --- a/app/api/rodauth_middleware.rb +++ b/app/api/rodauth_middleware.rb @@ -123,6 +123,9 @@ class RodauthMiddleware < Roda r.assets r.public + r.rodauth + I18n.locale = env["rack.locale"] + r.root do view(template: "root", layout: "hero") end @@ -132,7 +135,6 @@ class RodauthMiddleware < Roda r.is("privacy") do view(:privacy) end - r.rodauth check_csrf! unless r.content_type&.include?("application/json") || r.path.start_with?("/api/") # rodauth.load_oauth_application_management_routes diff --git a/config.ru b/config.ru index 5e50b70..875eccf 100644 --- a/config.ru +++ b/config.ru @@ -4,6 +4,7 @@ require_relative "preload" unless defined?(EnvironmentConfig) Rack::Request.ip_filter = ->(ip) { EnvironmentConfig.rack_trusted_ips_re.match?(ip) } use Rack::CommonLogger +use Rack::Locale use API::RodauthMiddleware run API::Base diff --git a/test/rack_helper.rb b/test/rack_helper.rb index d141900..99edebe 100644 --- a/test/rack_helper.rb +++ b/test/rack_helper.rb @@ -5,8 +5,8 @@ module RackHelper def self.app app = Rack::Builder.parse_file("config.ru") - # TODO: need a less hacky way to do this, maybe. - app.instance_variable_get(:@app).instance_variable_get(:@mid).rodauth.configure do + # TODO: need a less hacky way to do this, maybe. 😅 + app.instance_variable_get(:@app).instance_variable_get(:@app).instance_variable_get(:@mid).rodauth.configure do oauth_grants_token_hash_column nil oauth_grants_refresh_token_hash_column nil oauth_applications_client_secret_hash_column nil From d28e5332247b4ea7575049ae297568665ed6b7a4 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Mon, 14 Apr 2025 09:29:01 +0200 Subject: [PATCH 3/9] Get language from profile preferences, add test --- app/api/rodauth_middleware.rb | 3 ++- app/persistence/datasets.rb | 5 ++++ app/persistence/repository/account.rb | 9 +++++++ test/capybara/locale_test.rb | 34 +++++++++++++++++++++++++++ test/capybara_helper.rb | 2 +- test/factories/account.rb | 4 +++- test/factories/profile_preference.rb | 11 +++++++++ 7 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 test/capybara/locale_test.rb create mode 100644 test/factories/profile_preference.rb diff --git a/app/api/rodauth_middleware.rb b/app/api/rodauth_middleware.rb index 2ea6d6c..e103730 100644 --- a/app/api/rodauth_middleware.rb +++ b/app/api/rodauth_middleware.rb @@ -124,7 +124,8 @@ class RodauthMiddleware < Roda r.public r.rodauth - I18n.locale = env["rack.locale"] + profile_preferences = Persistence::Repository::Account.profile_preferences_from_account_id(account_id: rodauth.account!&.dig(:id)) + I18n.locale = profile_preferences&.dig("locale") || env["rack.locale"] r.root do view(template: "root", layout: "hero") diff --git a/app/persistence/datasets.rb b/app/persistence/datasets.rb index 2052e5f..3a6dbd0 100644 --- a/app/persistence/datasets.rb +++ b/app/persistence/datasets.rb @@ -26,6 +26,11 @@ def profile_blocks Database.connection[:profile_blocks] end + # @return [Sequel::Postgres::Dataset] + def profile_preferences + Database.connection[:profile_preferences] + end + # @return [Sequel::Postgres::Dataset] def conversations Database.connection[:conversations] diff --git a/app/persistence/repository/account.rb b/app/persistence/repository/account.rb index 9b9ebc8..b649a5a 100644 --- a/app/persistence/repository/account.rb +++ b/app/persistence/repository/account.rb @@ -197,6 +197,15 @@ def make_admin(email:) accounts.where(email:) .update(type: "admin") end + + # @return [Hash] + def profile_preferences_from_account_id(account_id:) + return {} unless account_id + + profile_preferences.where(profile_id: profiles.where(account_id:) + .select(:id)) + .get(:preferences) + end end end end diff --git a/test/capybara/locale_test.rb b/test/capybara/locale_test.rb new file mode 100644 index 0000000..5b8db6b --- /dev/null +++ b/test/capybara/locale_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "../capybara_helper" + +class LocaleTest < CapybaraTestCase + def login(login: Faker::Internet.email) + visit "/login" + + assert_text "Forgot Password?" + + fill_in("login", with: login) + fill_in("Password", with: "password") + within("form") do + click_on("Login") + end + + assert_text "You have been logged in" + end + + it "visits the main page and see buttons in english" do + visit "/" + + assert_text("Login") + assert_text("Sign up") + end + + it "Logs in with an account that has pt-br as preferences and sees buttons in portuguese" do + account = create(:account, profile_preference: { preferences: { locale: "pt-BR" } }) + login(login: account.email) + visit "/" + + assert_button("Encerrar sessão") + end +end diff --git a/test/capybara_helper.rb b/test/capybara_helper.rb index 82af24a..17bdba6 100644 --- a/test/capybara_helper.rb +++ b/test/capybara_helper.rb @@ -31,7 +31,7 @@ def selenium_request?(request) options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") - options.add_argument("--window-size=1400, 1400") + options.add_argument("--window-size=1400,1400") driver = Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) driver end diff --git a/test/factories/account.rb b/test/factories/account.rb index d082355..49f0c18 100644 --- a/test/factories/account.rb +++ b/test/factories/account.rb @@ -11,12 +11,14 @@ class Account < Sequel::Model transient do password { "password" } profile { {} } + profile_preference { {} } end email { Faker::Internet.email } account_status { AccountStatus.verified } after(:create) do |account, evaluator| create(:account_password_hash, account:, password: evaluator.password) - create(:profile, account:, **evaluator.profile) + profile = create(:profile, account:, **evaluator.profile) + create(:profile_preference, profile:, **evaluator.profile_preference) end end end diff --git a/test/factories/profile_preference.rb b/test/factories/profile_preference.rb new file mode 100644 index 0000000..3311b7c --- /dev/null +++ b/test/factories/profile_preference.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ProfilePreference < Sequel::Model + many_to_one :profile +end +FactoryBot.define do + factory :profile_preference do + profile + preferences { { locale: "en" } } + end +end From aa326250d7ccfda0b0b45ab1b840f0f8441cc082 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Tue, 15 Apr 2025 11:57:09 +0200 Subject: [PATCH 4/9] Remove commented-out plugin It is doubled, since it's uncommented a bit below --- app/api/rodauth_middleware.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/rodauth_middleware.rb b/app/api/rodauth_middleware.rb index e103730..49df1c8 100644 --- a/app/api/rodauth_middleware.rb +++ b/app/api/rodauth_middleware.rb @@ -14,7 +14,6 @@ module API class RodauthMiddleware < Roda plugin :public, root: File.expand_path("../assets/images", __dir__) plugin :flash - # plugin :middleware plugin :assets, css: "layout.scss", js: "base.js", path: File.expand_path("../assets", __dir__) plugin :render, views: File.expand_path("../assets/html", __dir__), engine: "haml", engine_opts: { "haml" => { escape_html: false } }, template_opts: { default_encoding: "UTF-8" } From 1d11e0b1a4da2505d56ab4d5efc14a3221b12b6d Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Tue, 15 Apr 2025 11:57:34 +0200 Subject: [PATCH 5/9] Allow language selection when not logged in --- app/api/rodauth_middleware.rb | 9 ++++++++- app/assets/html/header.haml | 11 +++++++++++ config/locales/en.yml | 1 + config/locales/pt-BR.yml | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/api/rodauth_middleware.rb b/app/api/rodauth_middleware.rb index 49df1c8..c044f79 100644 --- a/app/api/rodauth_middleware.rb +++ b/app/api/rodauth_middleware.rb @@ -124,7 +124,7 @@ class RodauthMiddleware < Roda r.rodauth profile_preferences = Persistence::Repository::Account.profile_preferences_from_account_id(account_id: rodauth.account!&.dig(:id)) - I18n.locale = profile_preferences&.dig("locale") || env["rack.locale"] + I18n.locale = profile_preferences&.dig("locale") || session["locale"] || env["rack.locale"] r.root do view(template: "root", layout: "hero") @@ -141,6 +141,13 @@ class RodauthMiddleware < Roda # rodauth.load_oauth_grant_management_routes rodauth.load_oauth_server_metadata_route # Loads .well-known/oauth-authorization-server path + r.is("language") do + r.post do + session["locale"] = r.params["locale"] if I18n.available_locales.include?(r.params["locale"].to_sym) + r.redirect("/") + end + end + # Supposed to catch any non-api paths that are not in rodauth # Should be the last one, otherwise other routes will also 404 # The regex is supposed to mean: diff --git a/app/assets/html/header.haml b/app/assets/html/header.haml index e25e796..35889fe 100644 --- a/app/assets/html/header.haml +++ b/app/assets/html/header.haml @@ -18,3 +18,14 @@ - else %a.navbar-item{href: "/login"}= I18n.t("login") %a.navbar-item{href: "/create-account"}= I18n.t("sign_up") + .navbar-item.has-dropdown.is-hoverable + .navbar-link + Language selection + .navbar-dropdown.is-right + - I18n.available_locales.each do |locale| + .navbar-item + %form{action: "/language", method: :post} + = csrf_tag("/language") + %input{type: "hidden", name: "locale", value: locale} + - I18n.with_locale(locale) do + %input{type: "submit", class: "button is-link is-light", value: I18n.t("language_name")} diff --git a/config/locales/en.yml b/config/locales/en.yml index b662b74..e384887 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,6 @@ --- en: + language_name: "English" about: "About" terms: "Terms of service" privacy_policy: Privacy policy diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 69f1a91..56ff990 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1,5 +1,6 @@ --- pt-BR: + language_name: "Português brasileiro" about: "Sobre" terms: "Terms de uso" privacy_policy: Política de Privacidade From 9138ef1c54dea7a1ba2de4a1a0f7e7b796fc7491 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Wed, 16 Apr 2025 16:02:23 +0200 Subject: [PATCH 6/9] Add fr to locales --- config/locales.rb | 2 +- config/locales/fr.yml | 7 + config/locales/pt-BR.yml | 1 + config/locales/rodauth.fr.yml | 247 ++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 config/locales/fr.yml create mode 100644 config/locales/rodauth.fr.yml diff --git a/config/locales.rb b/config/locales.rb index 2c50b4b..4b57b6c 100644 --- a/config/locales.rb +++ b/config/locales.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true I18n.load_path += Dir["#{File.expand_path("locales", __dir__)}/*.yml"] -I18n.available_locales = %i[en pt-BR] +I18n.available_locales = %i[fr en pt-BR].sort! I18n.default_locale = :en diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 0000000..21637c5 --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,7 @@ +--- +fr: + language_name: "Français" + login: Se connecter + sign_up: Créer un compte + create_account: + birth_date_help: Nous avons besoin de votre date de naissance pour garantir que vous êtes autorisé à accéder à notre service, conformément à nos conditions d'utilisation. Vous NE POUVEZ pas modifier cette date ultérieurement, mais vous pouvez rendre votre âge privé si vous le souhaitez. diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 56ff990..aad621a 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -4,6 +4,7 @@ pt-BR: about: "Sobre" terms: "Terms de uso" privacy_policy: Política de Privacidade + login: Fazer login sign_up: "Criar uma conta" create_account: birth_date_help: Precisamos da sua data de nascimento para garantir que você pode acessar nosso serviço, de acordo com nossos termos de uso. Você NÃO PODE mudar essa informação depois, mas você pode esconder a sua idade se você quiser. diff --git a/config/locales/rodauth.fr.yml b/config/locales/rodauth.fr.yml new file mode 100644 index 0000000..558e32c --- /dev/null +++ b/config/locales/rodauth.fr.yml @@ -0,0 +1,247 @@ +fr: + rodauth: + account_expiration_error_flash: Ce compte a expiré, connexion impossible + active_sessions_error_flash: Cette session a été déconnectée + add_recovery_codes_button: Ajouter codes de récupération pour l'authentification + add_recovery_codes_error_flash: Impossible d'ajouter des codes de récupération + add_recovery_codes_heading: "

Ajouter des codes de récupération

" + add_recovery_codes_page_title: Codes de récupération pour l'authentification + already_an_account_with_this_login_message: déjà un compte avec cet identifiant + attempt_to_create_unverified_account_error_flash: Le compte que vous avez essayé de créer est en attente de vérification + attempt_to_login_to_unverified_account_error_flash: Le compte auquel vous avez essayé de vous connecter est en attente de vérification + change_login_button: Modifier l'identifiant + change_login_error_flash: Erreur lors de la modification de votre identifiant + change_login_needs_verification_notice_flash: Un email vous a été envoyé avec un lien pour vérifier votre modification d'identifiant + change_login_notice_flash: Votre identifiant a été modifié + change_login_page_title: Modifier l'identifiant + change_password_button: Modifier le mot de passe + change_password_error_flash: Erreur lors de la modification de votre mot de passe + change_password_notice_flash: Votre mot de passe a été modifié + change_password_page_title: Modifier le mot de passe + close_account_button: Supprimer le compte + close_account_error_flash: Erreur lors de la suppression de votre compte + close_account_notice_flash: Votre compte a été supprimé + close_account_page_title: Supprimer le compte + confirm_password_button: Confirmer le mot de passe + confirm_password_error_flash: Erreur lors de la confirmation du mot de passe + confirm_password_link_text: Entrez votre mot de passe + confirm_password_notice_flash: Votre mot de passe a été confirmé + confirm_password_page_title: Confirmer le mot de passe + contains_null_byte_message: contient un byte nul + create_account_button: Créer un compte + create_account_error_flash: Erreur lors de la création de votre compte + create_account_link_text: Créer un compte + create_account_notice_flash: Votre compte a été créé + create_account_page_title: Créer un nouveau compte + email_auth_email_recently_sent_error_flash: Un email vous a été envoyé avec un lien pour vous connecter + email_auth_email_sent_notice_flash: Un email vous a été envoyé avec un lien pour accéder à votre compte + email_auth_email_subject: Lien de connexion + email_auth_error_flash: Erreur lors de la connexion + email_auth_page_title: Connexion + email_auth_request_button: Enovyer un lien de connexion par email + email_auth_request_error_flash: Erreur lors de la demande d'un lien de connexion par email + email_subject_prefix: '' + expired_jwt_access_token_message: jeton d'accès JWT expiré + global_logout_label: Déconnecter tous les sessions connectées ? + input_field_label_suffix: '' + invalid_jwt_format_error_message: format JWT invalide ou problème dans l'en-tête Authorization + invalid_password_message: mot de passe erroné + invalid_recovery_code_error_flash: Erreur d'authentification par code de récupération + invalid_recovery_code_message: Code de récupération erroné + json_non_post_error_message: méthode non-POST utilisée dans l'API JSON + json_not_accepted_error_message: En-tête Accept non pris en charge. Doit accepter "application/json" ou type de contenu compatible + jwt_refresh_invalid_token_message: jeton d'actualisation JWT erroné + jwt_refresh_without_access_token_message: aucun jeton d'accès JWT fourni durant l'actualisation + login_button: Se connecter + login_confirm_label: Confirmer %{login_label} + login_does_not_meet_requirements_message: identifiant invalide, ne répond pas aux exigences + login_error_flash: Erreur lors de la connexion + login_form_footer_links_heading: + login_label: Se connecter + login_lockout_error_flash: Ce compte est actuellement verrouillé et inaccessible + login_not_valid_email_message: n'est pas une adresse mail valide + login_notice_flash: Vous êtes connecté(e) + login_page_title: Se connecter + login_too_long_message: "%{login_maximum_length} caractères maximum" + login_too_many_bytes_message: "%{login_maximum_bytes} bytes maximum" + login_too_short_message: "%{login_minimum_length} caractères minimum" + logins_do_not_match_message: les identifiants ne correspondent pas + logout_button: Se déconnecter + logout_notice_flash: Vous êtes déconnecté(e) + logout_page_title: Se déconnecter + multi_phase_login_page_title: Se connecter + need_password_notice_flash: Identifiant reconnu, veuillez entrer votre mot de passe + new_password_label: Nouveau mot de passe + no_current_sms_code_error_flash: Aucun code SMS pour ce compte + no_matching_email_auth_key_error_flash: "Erreur lors de la connexion: clé d'authentification mail invalide" + no_matching_login_message: aucun identifiant correspondant + no_matching_reset_password_key_error_flash: 'Erreur lors de la réinitialisation du mot de passe: clé de réinitialisation erronée ou expirée' + no_matching_unlock_account_key_error_flash: 'Erreur lors du déverrouillage du compte: clé de déverrouillage erronée ou expirée' + no_matching_verify_account_key_error_flash: 'Erreur lors de la vérification du compte: clé de vérification erronée' + no_matching_verify_login_change_key_error_flash: "Erreur lors de la vérification de la modification de l'identifiant: clé erronée" + non_json_request_error_message: Seules les requêtes au format JSON sont autorisées + otp_already_setup_error_flash: Vous avec déjà activé l'authentification à deux facteurs + otp_auth_button: Utiliser l'authentification à deux facteurs + otp_auth_error_flash: Erreur lors de l'authentification à deux facteurs + otp_auth_form_footer: '' + otp_auth_label: Code d'authentification + otp_auth_link_text: Utiliser l'authentification à deux facteurs + otp_auth_page_title: Entrer le code d'authentification + otp_disable_button: Désactiver l'authentification à deux facteurs + otp_disable_error_flash: Erreur lors de la désactivation + otp_disable_link_text: Désactiver l'authentification à deux facteurs + otp_disable_notice_flash: L'authentification à deux facteurs est désactivée + otp_disable_page_title: Désactiver l'authentification à deux facteurs + otp_invalid_auth_code_message: Code d'authentification erroné + otp_invalid_secret_message: secret erroné + otp_lockout_error_flash: Utilisation du code d'authentification bloqué à cause d'un nombre d'essais trop important + otp_provisioning_uri_label: Url de Provisioning + otp_secret_label: Secret + otp_setup_button: Activer l'authentification à deux facteurs + otp_setup_error_flash: Erreur lors de l'activation de l'authentification à deux facteurs + otp_setup_link_text: Activer l'authentification à deux facteurs + otp_setup_notice_flash: Authentification à deux facteurs activée + otp_setup_page_title: Activer l'authentification à deux facteurs + password_authentication_required_error_flash: Vous devez confirmer votre mot de passe pour continuer + password_changed_email_subject: Mot de passe modifié + password_confirm_label: Confirmer %{password_label} + password_does_not_meet_requirements_message: mot de passe erroné, ne répond pas aux exigences + password_expiration_error_flash: Votre mot de passe a expiré et doit être modifié + password_in_dictionary_message: est un mot dans le dictionnaire + password_invalid_pattern_message: contient une séquence de caractères commune + password_is_one_of_the_most_common_message: est un mot de passe fréquemment utilisé + password_label: Mot de passe + password_not_changeable_yet_error_flash: Votre mot de passe ne peut pas encore être modifié + password_not_enough_character_groups_message: ne contient aucune majuscule, minuscule, ou chiffre + password_same_as_previous_password_message: identique au mot de passe précédent + password_too_many_repeating_characters_message: contient trop de caractères consécutifs identiques + password_too_long_message: "%{password_maximum_length} caractères maximum" + password_too_many_bytes_message: "%{password_maximum_bytes} bytes maximium" + password_too_short_message: "%{password_minimum_length} caractères minimum" + passwords_do_not_match_message: les mots de passe ne correspondent pas + recovery_auth_button: Authentification par code de récupération + recovery_auth_link_text: Authentification avec code de récupération + recovery_auth_page_title: Entrer code de récupération + recovery_codes_added_notice_flash: Des codes de récupération supplémentaires ont été ajoutés + recovery_codes_label: Code de récupération + recovery_codes_link_text: Afficher codes de récupération + recovery_codes_page_title: Afficher codes de récupération + remember_button: Modifier paramètre "Se souvenir de moi" + remember_disable_label: Désactiver "Se souvenir de moi" + remember_error_flash: Erreur lors de la modification du paramètre "Se souvenir de moi" + remember_forget_label: M'oublier + remember_notice_flash: Votre paramètre "Se souvenir de moi" a été modifié + remember_page_title: Modifier paramètre "Se souvenir de moi" + remember_remember_label: Se souvenir de moir + require_login_error_flash: Veuillez vous identifier pour continuer + resend_verify_account_page_title: Renvoyer email de vérification + reset_password_button: Réinitialiser mot de passe + reset_password_email_recently_sent_error_flash: Un email vous a été envoyé avec un lien pour réinitialiser votre mot de passe + reset_password_email_sent_notice_flash: Un email vous a été envoyé avec un lien pour réinitialiser le mot de passe de votre compte + reset_password_email_subject: Réinitialiser mot de passe + reset_password_error_flash: Erreur lors de la réinitialisation du mot de passe + reset_password_explanatory_text: "

Si vous avez oublié votre mot de passe, vous pouvez le réinitialiser:

" + reset_password_notice_flash: Votre mot de passe a été réinitialisé + reset_password_page_title: Réinitialiser mot de passe + reset_password_request_button: Demande de réinitialisation de mot de passe + reset_password_request_error_flash: Erreur lors de la demande de réinitialisation de mot de passe + reset_password_request_link_text: Mot de passe oublié ? + reset_password_request_page_title: Demande de réinitialisation de mot de passe + same_as_current_login_message: identique à l'identifiant actuel + same_as_existing_password_message: mot de passe erroné, identique au mot de passe actuel + session_expiration_error_flash: Cette session a expiré, veuillez vous reconnecter + single_session_error_flash: Cette session a été déconnecté car une autre session a été créée. + sms_already_setup_error_flash: L'authentification SMS a déjà été activée + sms_auth_button: Se connecter par SMS + sms_auth_link_text: Se connecter avec le code reçu par SMS + sms_auth_page_title: Se connecter par SMS + sms_code_label: Code reçu par SMS + sms_confirm_button: Confirmer le numéro de récupération SMS + sms_confirm_notice_flash: L'authentification SMS a été activée + sms_confirm_page_title: Confirmer le numéro de récupération SMS + sms_disable_button: Désactiver l'authentification par SMS + sms_disable_error_flash: Erreur lors de la désactivation + sms_disable_link_text: Désactiver l'authentification par SMS + sms_disable_notice_flash: L'authentification par SMS a été désactivée + sms_disable_page_title: Désactiver l'authentification par SMS + sms_invalid_code_error_flash: Erreur lors de l'authentification par code SMS + sms_invalid_code_message: code SMS invalide + sms_invalid_confirmation_code_error_flash: Code de confirmation SMS erroné ou expiré, veuillez réactiver l'authentification par SMS + sms_invalid_phone_message: Numéro de téléphone erroné + sms_lockout_error_flash: L'authentification par SMS a été verrouillée + sms_needs_confirmation_error_flash: L'authentification par SMS requiert une confirmation + sms_not_setup_error_flash: L'authentification par SMS n'est pas encore activée + sms_phone_label: Numéro de téléphone + sms_request_button: Envoyer un code par SMS + sms_request_notice_flash: Un code vous a été envoyé par SMS + sms_request_page_title: Envoyer un code par SMS + sms_setup_button: Ajouter numéro de téléphone de secours + sms_setup_error_flash: Erreur lors de l'activation de l'authentification par SMS + sms_setup_link_text: Activer l'authentification par SMS de secours + sms_setup_page_title: Ajouter numéro de téléphone de secours + two_factor_already_authenticated_error_flash: Vous avez déjà été authentifié avec l'authentification multi-facteur + two_factor_auth_notice_flash: Vous avez été authentifié avec l'authentification multi-facteur + two_factor_auth_page_title: S'authentifier avec un facteur supplémentaire + two_factor_disable_button: Désactiver toutes les méthodes d'authentification multi-facteur + two_factor_disable_error_flash: Impossible de désactiver toutes les méthodes d'authentification multi-facteur + two_factor_disable_link_text: Désactiver toutes les méthodes d'authentification multi-facteur + two_factor_disable_notice_flash: Toutes les méthodes d'authentification multi-facteur ont été désactivées + two_factor_disable_page_title: Désactiver toutes les méthodes d'authentification multi-facteur + two_factor_manage_page_title: Gérer l'Authentification Multi-facteur + two_factor_need_authentication_error_flash: Vous devez vous authentifier avec un facteur supplémentaire pour continuer + two_factor_not_setup_error_flash: Ce compte n'a pas activé l'authentification multi-facteur + two_factor_remove_heading: "

Désactiver l'authentification multi-facteur

" + two_factor_setup_heading: "

Activer l'authentification multi-facteur

" + unlock_account_button: Déverrouiller le compte + unlock_account_email_recently_sent_error_flash: Un email vous a été envoyé avec un lien pour déverrouiller votre compte + unlock_account_email_subject: Déverrouiller le compte + unlock_account_error_flash: Erreur lors du déverrouillage du compte + unlock_account_explanatory_text: "

Ce compte est verrouillé. Vous pouvez le déverrouiller:

" + unlock_account_notice_flash: Votre compte a été déverrouillé + unlock_account_page_title: Déverrouiller le compte + unlock_account_request_button: Demander le déverrouillage du compte + unlock_account_request_explanatory_text: "

Ce compte est verrouillé. Vous pouvez adresser une demande de déverrouillage:

" + unlock_account_request_notice_flash: Un email vous a été envoyé avec un lien pour déverrouiller votre compte + unlock_account_request_page_title: Demander le déverrouillage du compte + unverified_account_message: compte non vérifié, veuillez vérifier votre compte avant de vous connecter. + unverified_change_login_error_flash: Veuillez vérifier votre compte avant de changer l'identifiant + verify_account_button: Vérifier le compte + verify_account_email_recently_sent_error_flash: Un email vous a été envoyé avec un lien pour vérifier votre compte + verify_account_email_sent_notice_flash: Un email vous a été envoyé avec un lien pour vérifier votre compte + verify_account_email_subject: Vérifier le compte + verify_account_error_flash: Impossible de vérifier le compte + verify_account_notice_flash: Votre compte a été verifié + verify_account_page_title: Vérifier le compte + verify_account_resend_button: Renvoyer l'email de vérification + verify_account_resend_error_flash: Impossible de renvoyer l'email de vérification + verify_account_resend_explanatory_text: "

Si vous n'avez plus l'email de vérification, vous pouvez en demander un nouveau:

" + verify_account_resend_link_text: Renvoyer les informations pour vérifier le compte + verify_login_change_button: Vérifier le changement d'identifiant + verify_login_change_duplicate_account_error_flash: Impossible de changer l'identifiant, un compte avec cet identifiant existe déjà + verify_login_change_email_subject: Vérifier le changement d'identifiant + verify_login_change_error_flash: Impossible de vérifier le changement d'identifiant + verify_login_change_notice_flash: Votre changement d'identifiant a été vérifié + verify_login_change_page_title: Vérifier le changement d'identifiant + view_recovery_codes_button: Afficher les codes de récupération + view_recovery_codes_error_flash: Impossible d'afficher les codes de récupération + webauthn_auth_button: S'authentifier en utilisant WebAuthn + webauthn_auth_error_flash: Erreur lors de l'authentification avec WebAuthn + webauthn_auth_link_text: S'authentifier en utilisant WebAuthn + webauthn_auth_page_title: S'authentifier en utilisant WebAuthn + webauthn_duplicate_webauthn_id_message: tentative d’insérer un double de l’identifiant WebAuthn + webauthn_invalid_auth_param_message: paramètre d'authentification WebAuthn erroné + webauthn_invalid_remove_param_message: doit sélectionner un authentifiant valide pour supprimer + webauthn_invalid_setup_param_message: paramètre WebAuthn non valide + webauthn_invalid_sign_count_message: le mot de passe WebAuthn n’est pas valide + webauthn_login_error_flash: Erreur lors de l'authentification WebAuthn + webauthn_not_setup_error_flash: Ce compte n'a pas activé l'authentification WebAuthn + webauthn_remove_button: Désactiver l'authentification WebAuthn + webauthn_remove_error_flash: Erreur lors de la désactivation + webauthn_remove_link_text: Désactiver l'authentification WebAuthn + webauthn_remove_notice_flash: L'authentification WebAuthn a été désactivée + webauthn_remove_page_title: Désactiver l'authentification WebAuthn + webauthn_setup_button: Activer l'authentification WebAuthn + webauthn_setup_error_flash: Erreur lors de l'activation + webauthn_setup_link_text: Activer l'authentification WebAuthn + webauthn_setup_notice_flash: L'authentification WebAuthn est activée + webauthn_setup_page_title: Activer l'authentification WebAuthn From efbac205b05ca321af7d2f090140e2fcae9345b3 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Wed, 16 Apr 2025 16:02:36 +0200 Subject: [PATCH 7/9] Make sure rodauth pages also obey locale rules --- app/api/rodauth_middleware.rb | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/api/rodauth_middleware.rb b/app/api/rodauth_middleware.rb index c044f79..cfc7d9c 100644 --- a/app/api/rodauth_middleware.rb +++ b/app/api/rodauth_middleware.rb @@ -12,6 +12,25 @@ module API ].freeze # middleware responsible for authentication class RodauthMiddleware < Roda + class << self + # This function will choose the request locale. + # It use the +request_locale+ key in the +env+ variable if available, otherwise, it checks if the request + # is authenticated and if so takes the locale from the profile preferences. If not, will take from the session and lastly + # from the rack.locale + # It also memoizes the result to the env + # + # @param env [Hash] the env variable for the request + # @param session [Object] the session object from roda for the current request + # @param rodauth [Rodauth] the rodauth object for the current request + # @return [String] The request locale + def choose_request_locale(env, session, rodauth) + return env["request_locale"] if env["request_locale"] + + profile_preferences = Persistence::Repository::Account.profile_preferences_from_account_id(account_id: rodauth.account!&.dig(:id)) + profile_locale = profile_preferences["locale"] if profile_preferences&.key?("locale") + env["request_locale"] = profile_locale || session["locale"] || env["rack.locale"] + end + end plugin :public, root: File.expand_path("../assets/images", __dir__) plugin :flash plugin :assets, css: "layout.scss", js: "base.js", path: File.expand_path("../assets", __dir__) @@ -94,6 +113,10 @@ class RodauthMiddleware < Roda render "reset_password" end + before_rodauth do + I18n.locale = RodauthMiddleware.choose_request_locale(request.env, session, self) + end + before_register do # Before registering, rodauth allows to authorize the client. Currently we allow any client to register, since the idea is that anyone could make a client. # This is why the nil, but probably should verify for a logged in user in some cases. @@ -123,8 +146,7 @@ class RodauthMiddleware < Roda r.public r.rodauth - profile_preferences = Persistence::Repository::Account.profile_preferences_from_account_id(account_id: rodauth.account!&.dig(:id)) - I18n.locale = profile_preferences&.dig("locale") || session["locale"] || env["rack.locale"] + I18n.locale = RodauthMiddleware.choose_request_locale(env, session, rodauth) r.root do view(template: "root", layout: "hero") From b8c60e6d7e0415452d59e30212da1c884082e0d0 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Wed, 16 Apr 2025 16:02:51 +0200 Subject: [PATCH 8/9] Change locale choice button style --- app/assets/html/header.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/html/header.haml b/app/assets/html/header.haml index 35889fe..e8f1ff8 100644 --- a/app/assets/html/header.haml +++ b/app/assets/html/header.haml @@ -28,4 +28,4 @@ = csrf_tag("/language") %input{type: "hidden", name: "locale", value: locale} - I18n.with_locale(locale) do - %input{type: "submit", class: "button is-link is-light", value: I18n.t("language_name")} + %input{type: "submit", class: "button is-white", value: I18n.t("language_name")} From a5e97dd1747a4de5d7aa696f8de85b6bdb2b1e3b Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Wed, 16 Apr 2025 16:03:01 +0200 Subject: [PATCH 9/9] Fix page title --- app/assets/html/hero.haml | 2 +- app/assets/html/layout.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/html/hero.haml b/app/assets/html/hero.haml index 4fe29ae..c62f383 100644 --- a/app/assets/html/hero.haml +++ b/app/assets/html/hero.haml @@ -2,7 +2,7 @@ %html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %title RetromeetWeb + %title Retromeet %meta{:content => "width=device-width,initial-scale=1", :name => "viewport"}/ %meta{:content => "yes", :name => "apple-mobile-web-app-capable"}/ %meta{:content => "yes", :name => "mobile-web-app-capable"}/ diff --git a/app/assets/html/layout.haml b/app/assets/html/layout.haml index 9281f83..00be8a7 100644 --- a/app/assets/html/layout.haml +++ b/app/assets/html/layout.haml @@ -2,7 +2,7 @@ %html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ - %title RetromeetWeb + %title Retromeet %meta{:content => "width=device-width,initial-scale=1", :name => "viewport"}/ %meta{:content => "yes", :name => "apple-mobile-web-app-capable"}/ %meta{:content => "yes", :name => "mobile-web-app-capable"}/