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