diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3b68882..4827c9f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_user! before_action :ensure_onboarded, unless: :devise_controller? + before_action :setup_locale # Strong parameters for account. # Used for account creation and update. @@ -32,4 +33,12 @@ def after_sign_out_path_for(resource_or_scope) uri.query = nil # Remove any query parameters uri.to_s end + + # The locale is set to the user's locale if present, otherwise it is set to the default locale + # Get available locales and default_locale from Kiqr::Config + def setup_locale + I18n.default_locale = Kiqr::Config.default_locale + I18n.available_locales = Kiqr::Config.available_locales + I18n.locale = current_user&.locale&.to_sym || I18n.default_locale + end end diff --git a/app/controllers/users/preferences_controller.rb b/app/controllers/users/preferences_controller.rb new file mode 100644 index 0000000..a430466 --- /dev/null +++ b/app/controllers/users/preferences_controller.rb @@ -0,0 +1,27 @@ +class Users::PreferencesController < ApplicationController + def edit + @user = current_user + end + + def update + @user = current_user + if @user.update(preferences_params) + redirect_to edit_user_preferences_path, notice: t(".updated") + else + render :edit, status: :unprocessable_entity + end + end + + private + + def options_for_locale + I18n.available_locales.map do |locale| + [I18n.t("languages.#{locale}"), locale] + end + end + helper_method :options_for_locale + + def preferences_params + params.require(:user).permit(:time_zone, :locale) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 5ca6a1c..3b977c8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,12 @@ class User < ApplicationRecord has_many :account_users, dependent: :destroy has_many :accounts, through: :account_users + # Validate time zone format. + validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)} + + # Validate locale is a valid locale. + validates :locale, inclusion: {in: Kiqr::Config.available_locales.map(&:to_s)} + # Get the user's full name from their personal account. delegate :name, to: :personal_account diff --git a/app/views/partials/navigations/_settings.html.erb b/app/views/partials/navigations/_settings.html.erb index 3120805..9a7a0ef 100644 --- a/app/views/partials/navigations/_settings.html.erb +++ b/app/views/partials/navigations/_settings.html.erb @@ -25,6 +25,14 @@ +<%= render(PageLayouts::Settings::NavigationItem::Component.new( + label: t('.items.preferences.label'), + description: t('.items.preferences.description'), + icon: "fa fa-sliders", + path: edit_user_preferences_path, + active: current_base_path?(edit_user_preferences_path) +)) %> + <%= render(PageLayouts::Settings::NavigationItem::Component.new( label: t('.items.two_factor.label'), description: t('.items.two_factor.description'), diff --git a/app/views/users/preferences/_form.html.erb b/app/views/users/preferences/_form.html.erb new file mode 100644 index 0000000..963adf9 --- /dev/null +++ b/app/views/users/preferences/_form.html.erb @@ -0,0 +1,7 @@ +<%= simple_form_for(user, url: user_preferences_path, method: :patch) do |f| %> + <%= f.input :locale, placeholder: t(".locale.placeholder"), required: true, autofocus: true, prompt: t(".locale.prompt"), as: :select, collection: options_for_locale %> + <%= f.input :time_zone, placeholder: t(".timezone.placeholder"), required: true, autofocus: true %> +
+ <%= f.button :submit, t(".submit_button") %> +
+<% end %> diff --git a/app/views/users/preferences/edit.html.erb b/app/views/users/preferences/edit.html.erb new file mode 100644 index 0000000..f45fbe5 --- /dev/null +++ b/app/views/users/preferences/edit.html.erb @@ -0,0 +1,7 @@ +<% title t(".title") %> +<%= render(PageLayouts::Settings::Component.new( + title: t(".title"), + description: t(".description") +)) do %> + <%= render "form", user: @user %> +<% end %> diff --git a/config/initializers/kiqr.rb b/config/initializers/kiqr.rb index 7dbdac8..e2f477d 100644 --- a/config/initializers/kiqr.rb +++ b/config/initializers/kiqr.rb @@ -9,4 +9,13 @@ # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.default_from_email = "please-change-me-at-config-initializers@example.com" + + # ==> Locales + # Configure the available locales in the application. + # This is used to validate the locale of the user. + config.available_locales = [:en, :sv] + + # Default locale + # This is used to set the default locale for the application. + config.default_locale = :en end diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb new file mode 100644 index 0000000..dec9270 --- /dev/null +++ b/config/initializers/locale.rb @@ -0,0 +1,5 @@ +# Permitted locales available for the application +I18n.available_locales = Kiqr::Config.available_locales + +# Set default locale to something other than :en +I18n.default_locale = Kiqr::Config.default_locale diff --git a/config/locales/kiqr.en.yml b/config/locales/kiqr.en.yml index 6ff2cb1..dbbc84b 100644 --- a/config/locales/kiqr.en.yml +++ b/config/locales/kiqr.en.yml @@ -20,12 +20,18 @@ en: user: label: "Login credentials" description: "Change user email or password" + preferences: + label: "Preferences" + description: "Change your personal settings" two_factor: label: "Two-factor authentication" description: "One-time passwords" delete_user: label: "Delete user account" description: "Remove all user related data" + languages: + en: "English" + sv: "Swedish" accounts: new: heading: @@ -222,6 +228,21 @@ en: label: "One-time password" placeholder: "Enter the 6-digit code" verify_button: "Verify configuration" + preferences: + edit: + title: "User preferences" + description: "Update your user preferences." + form: + email: + label: "Email address" + placeholder: "Enter a valid email address" + locale: + label: "Language" + prompt: "Select a language" + placeholder: "Select your preferred language" + submit_button: "Save changes" + update: + updated: "User preferences have been updated." account_mailer: invitation_email: welcome: "Welcome to %{app_name}" diff --git a/config/routes/authentication.rb b/config/routes/authentication.rb index 1ad6a35..85abfa9 100644 --- a/config/routes/authentication.rb +++ b/config/routes/authentication.rb @@ -17,6 +17,8 @@ delete "two-factor/destroy" => "two_factor#destroy", :as => :destroy_two_factor get "delete" => "cancellations#show", :as => :delete_user_registration + + resource :preferences, only: %i[edit update], as: :user_preferences end scope module: :users, path: nil do diff --git a/db/migrate/20240408210931_add_locale_and_time_zone_to_users.rb b/db/migrate/20240408210931_add_locale_and_time_zone_to_users.rb new file mode 100644 index 0000000..665ba9a --- /dev/null +++ b/db/migrate/20240408210931_add_locale_and_time_zone_to_users.rb @@ -0,0 +1,6 @@ +class AddLocaleAndTimeZoneToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :locale, :string, default: "en" + add_column :users, :time_zone, :string, default: "UTC" + end +end diff --git a/db/schema.rb b/db/schema.rb index 0eab228..917447c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_08_105555) do +ActiveRecord::Schema[7.1].define(version: 2024_04_08_210931) do create_table "account_invitations", force: :cascade do |t| t.string "public_uid" t.integer "account_id", null: false @@ -72,6 +72,8 @@ t.integer "consumed_timestep" t.boolean "otp_required_for_login", default: false t.text "otp_backup_codes" + t.string "locale", default: "en" + t.string "time_zone", default: "UTC" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["personal_account_id"], name: "index_users_on_personal_account_id" diff --git a/lib/kiqr/config.rb b/lib/kiqr/config.rb index cd20fbb..365acde 100644 --- a/lib/kiqr/config.rb +++ b/lib/kiqr/config.rb @@ -12,5 +12,14 @@ class Config # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config_accessor :default_from_email, default: "please-change-me-at-config-initializers@example.com" + + # ==> Locales + # Configure the available locales in the application. + # This is used to validate the locale of the user. + config_accessor :available_locales, default: [:en] + + # Default locale + # This is used to set the default locale for the application. + config_accessor :default_locale, default: :en end end diff --git a/test/controllers/users/preferences_controller_test.rb b/test/controllers/users/preferences_controller_test.rb new file mode 100644 index 0000000..25c3629 --- /dev/null +++ b/test/controllers/users/preferences_controller_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class Users::PreferencesControllerTest < ActionDispatch::IntegrationTest + test "should get edit page" do + user = create(:user) + sign_in(user) + get edit_user_preferences_path + assert_response :success + end + + test "can update user preferences" do + user = create(:user, time_zone: "UTC", locale: "en") + sign_in(user) + + patch user_preferences_path, params: {user: {time_zone: "Stockholm", locale: "sv"}} + assert_redirected_to edit_user_preferences_path + assert_equal "Stockholm", user.reload.time_zone + assert_equal "sv", user.reload.locale + end +end