diff --git a/Gemfile b/Gemfile
index 8990e88a..ad6d4e78 100644
--- a/Gemfile
+++ b/Gemfile
@@ -34,6 +34,7 @@ group :development, :test do
gem "rubocop-rails-omakase", require: false
gem "dotenv"
gem "letter_opener"
+ gem "i18n-tasks"
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index c0eb3a73..5f12910a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -112,6 +112,13 @@ GEM
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
+ better_html (2.0.2)
+ actionview (>= 6.0)
+ activesupport (>= 6.0)
+ ast (~> 2.0)
+ erubi (~> 1.4)
+ parser (>= 2.4)
+ smart_properties
bigdecimal (3.1.6)
bindex (0.8.1)
bootsnap (1.18.3)
@@ -142,12 +149,24 @@ GEM
ffi (1.16.3)
globalid (1.2.1)
activesupport (>= 6.1)
+ highline (3.0.1)
hotwire-livereload (1.3.1)
actioncable (>= 6.0.0)
listen (>= 3.0.0)
railties (>= 6.0.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
+ i18n-tasks (1.0.13)
+ activesupport (>= 4.0.2)
+ ast (>= 2.1.0)
+ better_html (>= 1.0, < 3.0)
+ erubi
+ highline (>= 2.0.0)
+ i18n
+ parser (>= 3.2.2.1)
+ rails-i18n
+ rainbow (>= 2.2.2, < 4.0)
+ terminal-table (>= 1.5.1)
importmap-rails (2.0.1)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
@@ -238,6 +257,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
+ rails-i18n (7.0.8)
+ i18n (>= 0.7, < 2)
+ railties (>= 6.0.0, < 8)
rainbow (3.1.1)
rake (13.1.0)
rb-fsevent (0.11.2)
@@ -300,6 +322,7 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
+ smart_properties (1.17.0)
sorbet-runtime (0.5.11226)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
@@ -316,6 +339,8 @@ GEM
railties (>= 6.0.0)
tailwindcss-rails (2.3.0-x86_64-linux)
railties (>= 6.0.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
thor (1.3.0)
timeout (0.4.1)
tzinfo (2.0.6)
@@ -352,6 +377,7 @@ DEPENDENCIES
debug
dotenv
hotwire-livereload
+ i18n-tasks
importmap-rails
inline_svg
jbuilder
diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb
index be83ec3a..b7584890 100644
--- a/app/controllers/password_resets_controller.rb
+++ b/app/controllers/password_resets_controller.rb
@@ -1,6 +1,8 @@
class PasswordResetsController < ApplicationController
layout "auth"
+ before_action :set_user_by_token, only: :update
+
def new
end
@@ -12,7 +14,7 @@ class PasswordResetsController < ApplicationController
).password_reset.deliver_later
end
- redirect_to root_path, notice: "If an account with that email exists, we have sent a link to reset your password."
+ redirect_to root_path, notice: t(".requested")
end
def edit
@@ -20,7 +22,7 @@ class PasswordResetsController < ApplicationController
def update
if @user.update(password_params)
- redirect_to new_session_path, notice: "Your password has been reset."
+ redirect_to new_session_path, notice: t(".success")
else
render :edit, status: :unprocessable_entity
end
@@ -30,7 +32,7 @@ class PasswordResetsController < ApplicationController
def set_user_by_token
@user = User.find_by_token_for(password_reset: params[:token])
- redirect_to new_password_reset_path, alert: "Invalid token." unless @user.present?
+ redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?
end
def password_params
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index cf28c08e..b1e859c6 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -6,7 +6,7 @@ class PasswordsController < ApplicationController
def update
if current_user.update(password_params)
- redirect_to root_path, notice: "Your password has been updated successfully."
+ redirect_to root_path, notice: t(".success")
else
render :edit, status: :unprocessable_entity
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3973ddbf..886962e3 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -14,10 +14,10 @@ class RegistrationsController < ApplicationController
if @user.save
login @user
- flash[:notice] = "You have signed up successfully."
+ flash[:notice] = t(".success")
redirect_to root_path
else
- flash[:alert] = "Invalid input, please try again."
+ flash[:alert] = t(".failure")
render :new
end
end
@@ -34,7 +34,7 @@ class RegistrationsController < ApplicationController
def claim_invite_code
unless InviteCode.claim! params[:user][:invite_code]
- redirect_to new_registration_path, alert: "Invalid invite code, please try again."
+ redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
end
end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 468ab024..4bf52314 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -9,13 +9,13 @@ class SessionsController < ApplicationController
login user
redirect_to root_path
else
- flash.now[:alert] = "Invalid email or password."
+ flash.now[:alert] = t(".invalid_credentials")
render :new, status: :unprocessable_entity
end
end
def destroy
logout
- redirect_to root_path, notice: "You have signed out successfully."
+ redirect_to root_path, notice: t(".logout_successful")
end
end
diff --git a/app/helpers/auth_messages_helper.rb b/app/helpers/auth_messages_helper.rb
new file mode 100644
index 00000000..116ea654
--- /dev/null
+++ b/app/helpers/auth_messages_helper.rb
@@ -0,0 +1,6 @@
+module AuthMessagesHelper
+ def auth_messages(form = nil)
+ render "shared/auth_messages", flash: flash,
+ errors: form&.object&.errors&.full_messages || []
+ end
+end
diff --git a/app/views/layouts/auth.html.erb b/app/views/layouts/auth.html.erb
index 1b702ea2..14379632 100644
--- a/app/views/layouts/auth.html.erb
+++ b/app/views/layouts/auth.html.erb
@@ -18,12 +18,6 @@
- <% flash.each do |type, msg| %>
-
- <%= msg %>
-
- <% end %>
-
diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb
index 1ae6fbf1..90a94474 100644
--- a/app/views/password_resets/edit.html.erb
+++ b/app/views/password_resets/edit.html.erb
@@ -3,6 +3,7 @@
%>
<%= form_with url: password_reset_path(token: params[:token]), html: {class: 'space-y-6'} do |form| %>
+ <%= auth_messages form %>
<%= form.label :password, class: 'p-4 pb-0 block text-sm font-medium text-gray-700' %>
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb
index fe06a479..119f7464 100644
--- a/app/views/password_resets/new.html.erb
+++ b/app/views/password_resets/new.html.erb
@@ -3,6 +3,8 @@
%>
<%= form_with url: password_reset_path, html: {class: 'space-y-6'} do |form| %>
+ <%= auth_messages form %>
+
<%= form.label :email, class: 'p-4 pb-0 block text-sm font-medium text-gray-700' %>
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: 'required', placeholder: 'you@example.com', class: 'p-4 pt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full' %>
diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb
index 36691bbc..a40731fd 100644
--- a/app/views/passwords/edit.html.erb
+++ b/app/views/passwords/edit.html.erb
@@ -1,11 +1,7 @@
Update Password
<%= form_with model: current_user, url: password_path do |form| %>
- <% if form.object.errors.any? %>
- <% form.object.errors.full_messages.each do |message| %>
-
<%= message %>
- <% end %>
- <% end %>
+ <%= auth_messages form %>
<%= form.label :password_challenge, "Current Password" %>
diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb
index 5b21f599..64dde02b 100644
--- a/app/views/registrations/new.html.erb
+++ b/app/views/registrations/new.html.erb
@@ -3,11 +3,7 @@
%>
<%= form_with model: @user, url: registration_path, html: {class: 'space-y-6'} do |form| %>
- <% if form.object.errors.any? %>
- <% form.object.errors.full_messages.each do |message| %>
-
<%= message %>
- <% end %>
- <% end %>
+ <%= auth_messages form %>
<%= form.label :email, class: 'p-4 pb-0 block text-sm font-medium text-gray-700' %>
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
index 93acd91f..ff762ddb 100644
--- a/app/views/sessions/new.html.erb
+++ b/app/views/sessions/new.html.erb
@@ -3,6 +3,8 @@
%>
<%= form_with url: session_path, html: {class: 'space-y-6'} do |form| %>
+ <%= auth_messages form %>
+
<%= form.label :email, "Email address", class: 'p-4 pb-0 block text-sm font-medium text-gray-700' %>
<%= form.email_field :email, autofocus: false, autocomplete: "email", required: 'required', placeholder: 'you@example.com', class: 'p-4 pt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full' %>
diff --git a/app/views/shared/_auth_messages.html.erb b/app/views/shared/_auth_messages.html.erb
new file mode 100644
index 00000000..62883dca
--- /dev/null
+++ b/app/views/shared/_auth_messages.html.erb
@@ -0,0 +1,7 @@
+<% flash.each do |type, msg| %>
+
<%= msg %>
+<% end %>
+
+<% errors.each do |message| %>
+
<%= message %>
+<% end %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
new file mode 100644
index 00000000..c29ef601
--- /dev/null
+++ b/config/i18n-tasks.yml
@@ -0,0 +1,25 @@
+base_locale: en
+data:
+ read:
+ - config/locales/%{locale}.yml
+ write:
+ - config/locales/%{locale}.yml
+ router: conservative_router
+search:
+ paths:
+ - app/
+ relative_roots:
+ - app/controllers
+ - app/helpers
+ - app/mailers
+ - app/presenters
+ - app/views
+ strict: true
+ ## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
+ ## *.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less
+ ## *.yml *.json *.zip *.tar.gz *.swf *.flv *.mp3 *.wav *.flac *.webm *.mp4 *.ogg *.opus *.webp *.map *.xlsx
+ exclude:
+ - app/assets/images
+ - app/assets/fonts
+ - app/assets/videos
+ - app/assets/builds
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6c349ae5..527b4de8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,31 +1,22 @@
-# Files in the config/locales directory are used for internationalization and
-# are automatically loaded by Rails. If you want to use locales other than
-# English, add the necessary files in this directory.
-#
-# To use the locales, use `I18n.t`:
-#
-# I18n.t "hello"
-#
-# In views, this is aliased to just `t`:
-#
-# <%= t("hello") %>
-#
-# To use a different locale, set it with `I18n.locale`:
-#
-# I18n.locale = :es
-#
-# This would use the information in config/locales/es.yml.
-#
-# To learn more about the API, please read the Rails Internationalization guide
-# at https://guides.rubyonrails.org/i18n.html.
-#
-# Be aware that YAML interprets the following case-insensitive strings as
-# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
-# must be quoted to be interpreted as strings. For example:
-#
-# en:
-# "yes": yup
-# enabled: "ON"
-
+---
en:
- hello: "Hello world"
+ password_resets:
+ create:
+ requested: If an account with that email exists, we have sent a link to reset
+ your password.
+ update:
+ invalid_token: Invalid token.
+ success: Your password has been reset.
+ passwords:
+ update:
+ success: Your password has been updated successfully.
+ registrations:
+ create:
+ failure: Invalid input, please try again.
+ invalid_invite_code: Invalid invite code, please try again.
+ success: You have signed up successfully.
+ sessions:
+ create:
+ invalid_credentials: Invalid email or password.
+ destroy:
+ logout_successful: You have signed out successfully.
diff --git a/test/i18n_test.rb b/test/i18n_test.rb
new file mode 100644
index 00000000..56f05df7
--- /dev/null
+++ b/test/i18n_test.rb
@@ -0,0 +1,34 @@
+require "i18n/tasks"
+
+class I18nTest < ActiveSupport::TestCase
+ def setup
+ @i18n = I18n::Tasks::BaseTask.new
+ end
+
+ def test_no_missing_keys
+ missing_keys = @i18n.missing_keys
+ assert_empty missing_keys,
+ "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
+ end
+
+ def test_no_unused_keys
+ unused_keys = @i18n.unused_keys
+ assert_empty unused_keys,
+ "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
+ end
+
+ def test_files_are_normalized
+ non_normalized = @i18n.non_normalized_paths
+ error_message = "The following files need to be normalized:\n" \
+ "#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
+ "Please run `i18n-tasks normalize' to fix"
+ assert_empty non_normalized, error_message
+ end
+
+ def test_no_inconsistent_interpolations
+ inconsistent_interpolations = @i18n.inconsistent_interpolations
+ error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
+ "Please run `i18n-tasks check-consistent-interpolations' to show them"
+ assert_empty inconsistent_interpolations, error_message
+ end
+end