lato 3.25.0 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -20
  3. data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +8 -5
  4. data/app/assets/javascripts/lato/controllers/lato_action_controller.js +26 -15
  5. data/app/assets/javascripts/lato/controllers/lato_confirm_controller.js +25 -15
  6. data/app/assets/javascripts/lato/controllers/lato_copyclip_controller.js +28 -10
  7. data/app/assets/javascripts/lato/controllers/lato_form_controller.js +1 -0
  8. data/app/assets/javascripts/lato/controllers/lato_index_controller.js +4 -1
  9. data/app/controllers/concerns/lato/componentable.rb +20 -8
  10. data/app/controllers/concerns/lato/sessionable.rb +39 -10
  11. data/app/controllers/lato/account_controller.rb +10 -8
  12. data/app/controllers/lato/application_controller.rb +25 -1
  13. data/app/controllers/lato/authentication_controller.rb +74 -39
  14. data/app/helpers/lato/components_helper.rb +42 -16
  15. data/app/jobs/lato/active_storage_cleaner_job.rb +8 -7
  16. data/app/models/lato/invitation.rb +1 -1
  17. data/app/models/lato/operation.rb +6 -2
  18. data/app/models/lato/user.rb +39 -9
  19. data/app/views/lato/account/_alert-accepted-privacy-policy-version.html.erb +1 -1
  20. data/app/views/lato/account/_alert-accepted-terms-and-conditions-version.html.erb +1 -1
  21. data/app/views/lato/account/_form-authenticator.html.erb +7 -3
  22. data/app/views/lato/account/_form-password.html.erb +3 -2
  23. data/app/views/lato/account/_form-web3.html.erb +4 -3
  24. data/app/views/lato/account/_security-confirmation.html.erb +3 -3
  25. data/app/views/lato/authentication/_fields-registration.html.erb +8 -7
  26. data/app/views/lato/authentication/_form-authenticator.html.erb +2 -2
  27. data/app/views/lato/authentication/_form-signin.html.erb +2 -2
  28. data/app/views/lato/authentication/_form-verify-email.html.erb +3 -2
  29. data/app/views/lato/authentication/_form-web3-signin.html.erb +4 -3
  30. data/app/views/lato/components/_index.html.erb +25 -20
  31. data/app/views/lato/components/_navbar_nav_item.html.erb +1 -1
  32. data/app/views/lato/components/_operation.html.erb +4 -1
  33. data/app/views/lato/components/_page_head.html.erb +1 -1
  34. data/app/views/lato/components/_sidebar_nav_item.html.erb +2 -1
  35. data/app/views/lato/mailer/invitation/invite_mail.html.erb +1 -1
  36. data/app/views/lato/mailer/user/email_verification_mail.html.erb +1 -1
  37. data/app/views/lato/mailer/user/password_update_mail.html.erb +1 -1
  38. data/app/views/lato/mailer/user/signin_success_mail.html.erb +1 -1
  39. data/app/views/layouts/lato/_action.html.erb +2 -2
  40. data/app/views/layouts/lato/_aside-opener.html.erb +2 -2
  41. data/app/views/layouts/lato/_confirm.html.erb +6 -8
  42. data/app/views/layouts/lato/_feedbacks.html.erb +9 -8
  43. data/app/views/layouts/lato/_mailer-head_content.html.erb +1 -1
  44. data/app/views/layouts/lato/_network.html.erb +3 -3
  45. data/app/views/layouts/lato/application.html.erb +3 -2
  46. data/config/locales/en.yml +32 -0
  47. data/config/locales/fr.yml +29 -0
  48. data/config/locales/it.yml +29 -0
  49. data/config/locales/ro.yml +29 -0
  50. data/db/migrate/20261110000000_add_second_factor_counters_to_lato_users.rb +6 -0
  51. data/lib/lato/config.rb +4 -0
  52. data/lib/lato/engine.rb +5 -0
  53. data/lib/lato/version.rb +1 -1
  54. metadata +22 -15
@@ -1,12 +1,12 @@
1
1
  <div class="mb-3" data-controller="lato-account-security" data-lato-account-security-challenge-url-value="<%= lato.account_security_challenge_path %>" data-lato-account-security-webauthn-value="<%= user.webauthn_enabled? && !user.authenticator_enabled? %>">
2
- <%= form.label :current_password, "Current password", class: "form-label" %>
2
+ <%= form.label :current_password, I18n.t('lato.current_password'), class: "form-label" %>
3
3
  <%= form.password_field :current_password, class: "form-control", autocomplete: "current-password" %>
4
4
  <% if user.authenticator_enabled? %>
5
- <%= form.label :current_authenticator_code, "Current authenticator code", class: "form-label mt-2" %>
5
+ <%= form.label :current_authenticator_code, I18n.t('lato.current_authenticator_code'), class: "form-label mt-2" %>
6
6
  <%= form.text_field :current_authenticator_code, value: nil, class: "form-control", autocomplete: "one-time-code", inputmode: "numeric" %>
7
7
  <% elsif user.webauthn_enabled? %>
8
8
  <%= form.hidden_field :current_webauthn_credential, value: nil, data: { lato_account_security_target: "credential" } %>
9
- <p class="text-muted mt-2">Confirm with your registered security key when submitting.</p>
9
+ <p class="text-muted mt-2"><%= I18n.t('lato.security_key_confirmation_help') %></p>
10
10
  <% end %>
11
11
  <p class="text-danger mt-2" data-lato-account-security-target="error" role="alert"></p>
12
12
  </div>
@@ -7,31 +7,32 @@ email_readonly ||= false
7
7
  <div class="row">
8
8
  <div class="col col-12 col-md-6 mb-3">
9
9
  <%= lato_form_item_label form, :first_name %>
10
- <%= lato_form_item_input_text form, :first_name, required: true %>
10
+ <%= lato_form_item_input_text form, :first_name, required: true, autocomplete: 'given-name' %>
11
11
  </div>
12
12
 
13
13
  <div class="col col-12 col-md-6 mb-3">
14
14
  <%= lato_form_item_label form, :last_name %>
15
- <%= lato_form_item_input_text form, :last_name, required: true %>
15
+ <%= lato_form_item_input_text form, :last_name, required: true, autocomplete: 'family-name' %>
16
16
  </div>
17
17
 
18
18
  <div class="col col-12 mb-3">
19
19
  <%= lato_form_item_label form, :email %>
20
- <%= lato_form_item_input_email form, :email, required: true, readonly: email_readonly %>
20
+ <%= lato_form_item_input_email form, :email, required: true, readonly: email_readonly, autocomplete: 'email' %>
21
21
  </div>
22
22
 
23
23
  <div class="col col-12 col-md-6 mb-3">
24
24
  <%= lato_form_item_label form, :password %>
25
- <%= lato_form_item_input_password form, :password, required: true %>
25
+ <%= lato_form_item_input_password form, :password, required: true, autocomplete: 'new-password', minlength: Lato.config.auth_password_min_length, aria: { describedby: form.field_id(:password, :help) } %>
26
+ <div class="form-text" id="<%= form.field_id(:password, :help) %>"><%= I18n.t('lato.password_requirements', count: Lato.config.auth_password_min_length) %></div>
26
27
  </div>
27
28
 
28
29
  <div class="col col-12 col-md-6 mb-3">
29
30
  <%= lato_form_item_label form, :password_confirmation %>
30
- <%= lato_form_item_input_password form, :password_confirmation, required: true %>
31
+ <%= lato_form_item_input_password form, :password_confirmation, required: true, autocomplete: 'new-password' %>
31
32
  </div>
32
33
  </div>
33
34
 
34
35
  <div class="mb-3 text-muted" style="font-size: 14px;">
35
- <%= lato_form_item_input_check form, :accepted_privacy_policy_version, I18n.t('lato.privacy_policy_checkbox', link: link_to(I18n.t('lato.privacy_policy'), Lato.config.legal_privacy_policy_url, target: '_blank')), required: true %>
36
- <%= lato_form_item_input_check form, :accepted_terms_and_conditions_version, I18n.t('lato.terms_and_conditions_checkbox', link: link_to(I18n.t('lato.terms_and_conditions'), Lato.config.legal_terms_and_conditions_url, target: '_blank')), required: true %>
36
+ <%= lato_form_item_input_check form, :accepted_privacy_policy_version, I18n.t('lato.privacy_policy_checkbox', link: link_to(I18n.t('lato.privacy_policy'), Lato.config.legal_privacy_policy_url, target: '_blank')).html_safe, required: true %>
37
+ <%= lato_form_item_input_check form, :accepted_terms_and_conditions_version, I18n.t('lato.terms_and_conditions_checkbox', link: link_to(I18n.t('lato.terms_and_conditions'), Lato.config.legal_terms_and_conditions_url, target: '_blank')).html_safe, required: true %>
37
38
  </div>
@@ -10,11 +10,11 @@ user ||= Lato::User.new
10
10
  <%= lato_form_errors user, class: %w[mb-3] %>
11
11
 
12
12
  <div class="mb-3 text-center">
13
- <p><%= raw I18n.t('lato.authenticator_code_help', email: h(user.email_protected)) %></p>
13
+ <%= form.label :authenticator_code, raw(I18n.t('lato.authenticator_code_help', email: h(user.email_protected))), class: 'form-label' %>
14
14
  </div>
15
15
 
16
16
  <div class="mb-3">
17
- <%= lato_form_item_input_text form, :authenticator_code, required: true %>
17
+ <%= lato_form_item_input_text form, :authenticator_code, required: true, autocomplete: 'one-time-code', inputmode: 'numeric', pattern: '[0-9]{6}', maxlength: 6, autofocus: true %>
18
18
  </div>
19
19
 
20
20
  <div>
@@ -11,12 +11,12 @@ user ||= Lato::User.new
11
11
 
12
12
  <div class="mb-3">
13
13
  <%= lato_form_item_label form, :email %>
14
- <%= lato_form_item_input_email form, :email, required: true %>
14
+ <%= lato_form_item_input_email form, :email, required: true, autocomplete: 'username' %>
15
15
  </div>
16
16
 
17
17
  <div class="mb-3">
18
18
  <%= lato_form_item_label form, :password %>
19
- <%= lato_form_item_input_password form, :password, required: true %>
19
+ <%= lato_form_item_input_password form, :password, required: true, autocomplete: 'current-password' %>
20
20
  </div>
21
21
 
22
22
  <div>
@@ -10,13 +10,14 @@ code ||= nil
10
10
  <%= form_with model: user, url: lato.authentication_verify_email_action_path(id: user.id), class: 'text-center', data: { turbo_frame: '_self' } do |form| %>
11
11
  <% if code %>
12
12
  <%= form.hidden_field :code, value: code %>
13
- <button type="submit" style="display: none;"></button>
13
+ <%# Visible fallback when JavaScript is disabled or blocked by CSP. %>
14
+ <button type="submit" class="btn btn-primary"><%= I18n.t('lato.verify_email') %></button>
14
15
  <script>document.getElementById('authentication_form_verify_email').querySelector('button[type="submit"]').click()</script>
15
16
  <% else %>
16
17
  <%= lato_form_notices class: %w[mb-3], fixed: true %>
17
18
  <%= lato_form_errors user, class: %w[mb-3], fixed: true %>
18
19
 
19
- <%= link_to 'Torna all tuo account', lato.account_path, class: %w[btn btn-primary] %>
20
+ <%= link_to I18n.t('lato.back_to_account'), lato.account_path, class: %w[btn btn-primary] %>
20
21
  <% end %>
21
22
  <% end %>
22
23
  <% end %>
@@ -12,7 +12,7 @@ user ||= Lato::User.new
12
12
  <% if session[:web3_nonce] %>
13
13
 
14
14
  <div class="alert alert-light mb-0 text-center">
15
- <h4 class="alert-heading">Connecting..</h4>
15
+ <h4 class="alert-heading" role="status"><%= I18n.t('lato.connecting') %></h4>
16
16
  <div class="progress" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
17
17
  <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
18
18
  </div>
@@ -21,13 +21,14 @@ user ||= Lato::User.new
21
21
  <span id="account_form-web3__nonce" style="display: none;"><%= session[:web3_nonce] %></span>
22
22
  <%= form.hidden_field :web3_address, id: 'account_form-web3__input-web3_address' %>
23
23
  <%= form.hidden_field :web3_signed_nonce, id: 'account_form-web3__input-web3_signed_nonce' %>
24
- <%= lato_form_submit form, 'Confirm', class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
24
+ <%= lato_form_submit form, I18n.t('lato.confirm'), class: %w[btn-primary d-none], id: 'account_form-web3__submit' %>
25
25
 
26
26
  <script>
27
27
  (async () => {
28
+ // Pinned exact version: dynamic import() can not carry an SRI hash (see README security checklist).
28
29
  const ethers = await import('https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.esm.min.js')
29
30
 
30
- const nonce = document.getElementById('account_form-web3__nonce').innerText
31
+ const nonce = document.getElementById('account_form-web3__nonce').textContent
31
32
  const inputAddress = document.getElementById('account_form-web3__input-web3_address')
32
33
  const inputSignedNonce = document.getElementById('account_form-web3__input-web3_signed_nonce')
33
34
  const submitButton = document.getElementById('account_form-web3__submit')
@@ -9,9 +9,10 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
9
9
 
10
10
  <%= turbo_frame_tag "lato_index_#{key}_#{model_name_underscore}", class: "lato-index #{browser.device.mobile? ? 'lato-index-mob' : 'lato-index-desk'}" do %>
11
11
 
12
- <%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', turbo_frame: '_self' } do %>
12
+ <%= form_tag request.path, method: :get, data: { controller: 'lato-form lato-index', lato_index_key_value: key, turbo_frame: '_self' } do %>
13
13
  <%= hidden_field_tag :key, key %>
14
- <% params.each do |k, v| %>
14
+ <% params.except(:controller, :action, :key).each do |k, v| %>
15
+ <% next unless v.is_a?(String) %>
15
16
  <%= hidden_field_tag k, v %>
16
17
  <% end %>
17
18
 
@@ -33,7 +34,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
33
34
  <% if custom_actions.any? %>
34
35
  <div class="btn-group ms-2">
35
36
  <% custom_actions.each do |action_key, action_params| %>
36
- <%= link_to action_params[:path], { class: 'btn btn-primary' }.merge(action_params) do %>
37
+ <%= link_to action_params[:path], { class: 'btn btn-primary', 'aria-label': action_params[:title] }.merge(action_params) do %>
37
38
  <i class="<%= action_params[:icon] %>"></i>
38
39
  <% end %>
39
40
  <% end %>
@@ -42,7 +43,7 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
42
43
 
43
44
  <% if dropdown_actions %>
44
45
  <div class="dropdown ms-2">
45
- <%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown' }.merge(dropdown_actions) do %>
46
+ <%= content_tag :button, { class: 'btn btn-primary dropdown-toggle', 'data-bs-toggle': 'dropdown', 'aria-label': dropdown_actions[:title] }.merge(dropdown_actions) do %>
46
47
  <i class="<%= dropdown_actions[:icon] %>"></i>
47
48
  <% end %>
48
49
  <ul class="dropdown-menu">
@@ -109,28 +110,32 @@ total_pages = collection.empty? ? params["#{key}_page"]&.to_i : 999_999_999 if s
109
110
  <thead>
110
111
  <tr class="align-middle">
111
112
  <% columns.each do |column| %>
112
- <th scope="col" class="lato-index-desk-col-label lato-index-desk-col-label-<%= column %>">
113
+ <% column_label = collection_test_istance.respond_to?(column) ? collection.model.human_attribute_name(column) : lato_index_dynamic_label({
114
+ key: key,
115
+ model_name: model_name,
116
+ column: column
117
+ }) %>
118
+ <% sort_direction = sortable_columns.include?(column) ? { "#{column}|ASC" => 'ascending', "#{column}|DESC" => 'descending' }[params["#{key}_sort_by"]] : nil %>
119
+ <%= content_tag :th, scope: 'col', class: "lato-index-desk-col-label lato-index-desk-col-label-#{column}", 'aria-sort': sort_direction do %>
113
120
  <div class="d-flex align-items-center justify-content-between">
114
- <span><%= collection_test_istance.respond_to?(column) ? collection.model.human_attribute_name(column) : lato_index_dynamic_label({
115
- key: key,
116
- model_name: model_name,
117
- column: column
118
- }) %></span>
121
+ <span><%= column_label %></span>
119
122
  <% if sortable_columns.include?(column) %>
120
123
  <div class="btn-group ms-3 lato-index-col-sort">
121
- <div class="position-relative btn btn-sm <%= params["#{key}_sort_by"] == "#{column}|ASC" ? 'btn-primary' : 'btn-outline-primary' %>">
122
- <input type="submit" name="<%= "#{key}_sort_by" %>" value="<%= column %>|ASC" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0">
123
- <i class="bi bi-sort-up"></i>
124
- </div>
125
-
126
- <div class="position-relative btn btn-sm <%= params["#{key}_sort_by"] == "#{column}|DESC" ? 'btn-primary' : 'btn-outline-primary' %>">
127
- <input type="submit" name="<%= "#{key}_sort_by" %>" value="<%= column %>|DESC" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0">
128
- <i class="bi bi-sort-down"></i>
129
- </div>
124
+ <% [['ASC', 'ascending', 'bi-sort-up'], ['DESC', 'descending', 'bi-sort-down']].each do |direction, direction_name, icon| %>
125
+ <% active = params["#{key}_sort_by"] == "#{column}|#{direction}" %>
126
+ <button
127
+ type="submit"
128
+ name="<%= "#{key}_sort_by" %>"
129
+ value="<%= column %>|<%= direction %>"
130
+ class="btn btn-sm <%= active ? 'btn-primary' : 'btn-outline-primary' %>"
131
+ aria-label="<%= I18n.t("lato.sort_#{direction_name}", column: strip_tags(column_label.to_s)) %>"
132
+ aria-pressed="<%= active %>"
133
+ ><i class="bi <%= icon %>" aria-hidden="true"></i></button>
134
+ <% end %>
130
135
  </div>
131
136
  <% end %>
132
137
  </div>
133
- </th>
138
+ <% end %>
134
139
  <% end %>
135
140
  </tr>
136
141
  </thead>
@@ -1,5 +1,5 @@
1
1
  <li class="nav-item">
2
- <%= link_to path, class: ['nav-link', ('active' if active)] do %>
2
+ <%= link_to path, class: ['nav-link', ('active' if active)], aria: { current: ('page' if active) } do %>
3
3
  <%= yield %>
4
4
  <% end %>
5
5
  </li>
@@ -47,7 +47,10 @@
47
47
 
48
48
  <% if operation.output_action? %>
49
49
  <div class="mt-3 d-flex justify-content-center">
50
- <%= link_to operation.output_action['label'], operation.output_action['href'], { class: 'btn btn-primary' }.merge(operation.output_action['attributes']) %>
50
+ <%# Only http(s) and relative links: a javascript: href built from job input would run on click. %>
51
+ <% output_href = operation.output_action['href'].to_s %>
52
+ <% output_href = '#' unless output_href.start_with?('/', 'http://', 'https://') && !output_href.start_with?('//') %>
53
+ <%= link_to operation.output_action['label'], output_href, { class: 'btn btn-primary' }.merge(operation.output_action['attributes'] || {}) %>
51
54
  </div>
52
55
  <% end %>
53
56
  </div>
@@ -9,7 +9,7 @@
9
9
  </ol>
10
10
  <% end %>
11
11
 
12
- <h1><%= raw title %></h1>
12
+ <h1><%= title %></h1>
13
13
 
14
14
  <%= yield %>
15
15
  </div>
@@ -9,6 +9,7 @@ unless has_children
9
9
  class: ['nav-link position-relative', (active ? 'active' : 'link-dark')].join(' '),
10
10
  }
11
11
  link_to_options[:target] = '_blank' if external
12
+ link_to_options[:aria] = { current: 'page' } if active
12
13
  end
13
14
 
14
15
  %>
@@ -28,7 +29,7 @@ end
28
29
  <ul class="list-unstyled mt-2 border-start border-1 ps-2">
29
30
  <% children.each do |child| %>
30
31
  <li class="py-1">
31
- <%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' ') do %>
32
+ <%= link_to child[:path], class: ['nav-link py-1', (active == child[:key] ? 'active' : 'link-dark')].join(' '), aria: { current: ('page' if active == child[:key]) } do %>
32
33
  <%= child[:label] %>
33
34
  <% end %>
34
35
  </li>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  You have received an invitation to use <%= Lato.config.application_title %>.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  To have access to all the features of <%= Lato.config.application_title %> you must confirm your email address.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  An update to your <%= Lato.config.application_title %> account password has been requested.
4
4
  <br><br>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
 
3
3
  An access to your <b><%= Lato.config.application_title %></b> account has been made with the IP address <b><%= @ip_address %></b>.
4
4
  <br><br>
@@ -1,11 +1,11 @@
1
1
  <% 10.times do %>
2
2
 
3
- <div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
3
+ <div class="modal fade" data-lato-action-target="modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
4
4
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" data-lato-action-target="modalDialog">
5
5
  <div class="modal-content">
6
6
  <div class="modal-header">
7
7
  <h2 class="mb-0 mt-0 fs-4" data-lato-action-target="modalTitle"></h2>
8
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
8
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= I18n.t('lato.close') %>"></button>
9
9
  </div>
10
10
  <div class="modal-body" data-lato-action-target="modalBody">
11
11
  ...
@@ -2,7 +2,7 @@
2
2
  class="aside-opener shadow"
3
3
  data-controller="lato-aside-opener"
4
4
  data-action="click->lato-aside-opener#onClickToggle"
5
- aria-label="Toggle sidebar"
5
+ aria-label="<%= I18n.t('lato.toggle_sidebar') %>"
6
6
  >
7
- <i class="bi bi-layout-text-sidebar-reverse"></i>
7
+ <i class="bi bi-layout-text-sidebar-reverse" aria-hidden="true"></i>
8
8
  </button>
@@ -1,26 +1,24 @@
1
- <div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true" data-bs-keyboard="false">
1
+ <div class="lato-confirm modal fade" data-controller="lato-confirm" tabindex="-1" aria-hidden="true" aria-labelledby="latoConfirmTitle">
2
2
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
3
3
  <div class="modal-content">
4
4
  <div class="modal-header">
5
- <h2 class="mb-0 mt-0 fs-4"><%= I18n.t('lato.confirm_title') %></h2>
6
- <button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="Close"></button>
5
+ <h2 class="mb-0 mt-0 fs-4" id="latoConfirmTitle"><%= I18n.t('lato.confirm_title') %></h2>
6
+ <button type="button" class="btn-close" data-action="click->lato-confirm#confirmCancel" aria-label="<%= I18n.t('lato.close') %>"></button>
7
7
  </div>
8
8
  <div class="modal-body" data-lato-confirm-target="modalBody">
9
9
  </div>
10
10
  <div class="modal-footer">
11
11
  <button
12
12
  type="button"
13
- class="btn btn-danger"
13
+ class="btn btn-secondary"
14
14
  data-action="click->lato-confirm#confirmCancel"
15
- aria-label="<%= I18n.t('lato.confirm_no') %>"
16
15
  ><%= I18n.t('lato.confirm_no') %></button>
17
16
  <button
18
17
  type="button"
19
- class="btn btn-success"
18
+ class="btn btn-primary"
20
19
  data-action="click->lato-confirm#confirmSuccess"
21
- aria-label="<%= I18n.t('lato.confirm_yes') %>"
22
20
  ><%= I18n.t('lato.confirm_yes') %></button>
23
21
  </div>
24
22
  </div>
25
23
  </div>
26
- </div>
24
+ </div>
@@ -1,11 +1,11 @@
1
1
  <div class="toast-container">
2
2
  <% if notice %>
3
3
  <div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
4
- <div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
4
+ <div data-controller="lato-feedback" id="feedbackNotice" class="toast hide" role="status" aria-live="polite" aria-atomic="true">
5
5
  <div class="toast-header">
6
- <i class="bi bi-check-circle-fill text-success me-2"></i>
7
- <strong class="me-auto"><%= I18n.t('lato.operation_completed') %></strong>
8
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
6
+ <i class="bi bi-check-circle-fill text-success me-2" aria-hidden="true"></i>
7
+ <strong class="me-auto"><%= I18n.t('lato.notice_title') %></strong>
8
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
9
9
  </div>
10
10
  <div class="toast-body">
11
11
  <%= notice %>
@@ -16,11 +16,12 @@
16
16
 
17
17
  <% if alert %>
18
18
  <div class="position-fixed end-0 bottom-0 p-3" style="z-index: 11;">
19
- <div data-controller="lato-feedback" id="feedbackAlert" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
19
+ <%# Errors stay visible until dismissed: auto hiding gives no time to read them. %>
20
+ <div data-controller="lato-feedback" id="feedbackAlert" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
20
21
  <div class="toast-header">
21
- <i class="bi bi-check-circle-fill text-danger me-2"></i>
22
- <strong class="me-auto"><%= I18n.t('lato.operation_failed') %></strong>
23
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
22
+ <i class="bi bi-exclamation-triangle-fill text-danger me-2" aria-hidden="true"></i>
23
+ <strong class="me-auto"><%= I18n.t('lato.alert_title') %></strong>
24
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="<%= I18n.t('lato.close') %>"></button>
24
25
  </div>
25
26
  <div class="toast-body">
26
27
  <%= alert %>
@@ -1,4 +1,4 @@
1
- <% if I18n.locale == 'en' %>
1
+ <% if I18n.locale.to_s != 'it' %>
2
2
  Hello,
3
3
  <br><br>
4
4
  <% else %>
@@ -1,4 +1,4 @@
1
- <div class="fixed-bottom p-3 d-none" data-controller="lato-network">
2
- <span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline">You are offline</span>
3
- <span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online">You are online</span>
1
+ <div class="fixed-bottom p-3 d-none" data-controller="lato-network" role="status" aria-live="polite">
2
+ <span class="badge rounded-pill text-bg-danger fs-6 d-none" data-lato-network-target="offline"><%= I18n.t('lato.network_offline') %></span>
3
+ <span class="badge rounded-pill text-bg-success fs-6 d-none" data-lato-network-target="online"><%= I18n.t('lato.network_online') %></span>
4
4
  </div>
@@ -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,shrink-to-fit=no,interactive-widget=resizes-content">
6
- <title><%= @layout_page_title %></title>
6
+ <title><%= @layout_page_title.presence || Lato.config.application_title %></title>
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
 
@@ -27,6 +27,7 @@
27
27
  class="controller-<%= controller_name %> action-<%= action_name %> <%= @layout_body_classes.join(' ') %> <%= cookies[:lato_aside_hidden] == 'true' ? 'aside-hidden' : '' %>"
28
28
  data-controller="lato-action lato-guide"
29
29
  >
30
+ <a class="visually-hidden-focusable position-absolute top-0 start-0 p-2 bg-white" style="z-index: 2100;" href="#main"><%= I18n.t('lato.skip_to_content') %></a>
30
31
  <header>
31
32
  <%= render 'layouts/lato/navbar' %>
32
33
  </header>
@@ -39,7 +40,7 @@
39
40
  </aside>
40
41
  <% end %>
41
42
 
42
- <main class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
43
+ <main id="main" tabindex="-1" class="<%= @layout_sidebar ? 'layout_sidebar' : '' %>">
43
44
  <%= render 'layouts/lato/content' %>
44
45
  </main>
45
46
  </div>
@@ -1,5 +1,28 @@
1
1
  en:
2
2
  lato:
3
+ close: "Close"
4
+ notice_title: "Notification"
5
+ alert_title: "Error"
6
+ sort_ascending: "Sort by %{column} ascending"
7
+ sort_descending: "Sort by %{column} descending"
8
+ password_requirements: "At least %{count} characters."
9
+ too_many_requests: "Too many requests, please wait %{minutes} minutes to retry."
10
+ hcaptcha_missing: "Please complete the captcha verification."
11
+ hcaptcha_failed: "Captcha verification failed, please retry."
12
+ security_confirmation_required: "Confirm your current password and enabled second factor to continue."
13
+ current_password: "Current password"
14
+ current_authenticator_code: "Current authenticator code"
15
+ security_key_confirmation_help: "Confirm with your registered security key when submitting."
16
+ confirm_authenticator_code: "Confirm authenticator code"
17
+ authenticator_qr_code_alt: "Authenticator enrollment QR code"
18
+ authenticator_manual_key: "Or enter this key manually in your authenticator app"
19
+ connecting: "Connecting..."
20
+ back_to_account: "Back to your account"
21
+ network_offline: "You are offline"
22
+ network_online: "You are online"
23
+ toggle_sidebar: "Toggle sidebar"
24
+ skip_to_content: "Skip to main content"
25
+ copied: "Copied to clipboard"
3
26
  account: Account
4
27
  signout: Logout
5
28
  signin: Login
@@ -144,6 +167,7 @@ en:
144
167
  credentials_not_correct: Email or password not correct
145
168
  privacy_policy_invalid: To accept the privacy policy you must select the confirmation checkbox
146
169
  terms_and_conditions_invalid: To accept the terms and conditions you must select the confirmation checkbox
170
+ invitation_invalid: Invalid invitation
147
171
  web3_address_invalid: The address you send is not corretly signed
148
172
  web3_connection_error: Impossible to connect the wallet
149
173
  authenticator_code_invalid: The code you inserted is not correct
@@ -153,6 +177,8 @@ en:
153
177
  webauthn_authentication_failed: Passkey authentication failed. Please try again or use another method.
154
178
  password:
155
179
  not_correct: not correct
180
+ password_confirmation:
181
+ confirmation: does not match the password
156
182
  email:
157
183
  taken: already used with another account
158
184
  not_registered: not registered
@@ -163,8 +189,14 @@ en:
163
189
  inclusion: not accepted
164
190
  web3_address:
165
191
  not_correct: not correct
192
+ lato/operation:
193
+ attributes:
194
+ base:
195
+ start_error: "Unable to start the job"
166
196
  lato/invitation:
167
197
  attributes:
198
+ email:
199
+ taken: "already used by another user"
168
200
  base:
169
201
  already_accepted: Invitation already accepted
170
202
  email_sending_error: Unable to send mail
@@ -2,6 +2,29 @@
2
2
 
3
3
  fr:
4
4
  lato:
5
+ close: "Fermer"
6
+ notice_title: "Notification"
7
+ alert_title: "Erreur"
8
+ sort_ascending: "Trier par %{column} croissant"
9
+ sort_descending: "Trier par %{column} d\u00E9croissant"
10
+ password_requirements: "Au moins %{count} caract\u00E8res."
11
+ too_many_requests: "Trop de requ\u00EAtes, veuillez attendre %{minutes} minutes avant de r\u00E9essayer."
12
+ hcaptcha_missing: "Veuillez compl\u00E9ter la v\u00E9rification captcha."
13
+ hcaptcha_failed: "La v\u00E9rification captcha a \u00E9chou\u00E9, veuillez r\u00E9essayer."
14
+ security_confirmation_required: "Confirmez votre mot de passe actuel et le second facteur activ\u00E9 pour continuer."
15
+ current_password: "Mot de passe actuel"
16
+ current_authenticator_code: "Code authenticator actuel"
17
+ security_key_confirmation_help: "Confirmez avec votre cl\u00E9 de s\u00E9curit\u00E9 enregistr\u00E9e lors de l'envoi."
18
+ confirm_authenticator_code: "Confirmer le code authenticator"
19
+ authenticator_qr_code_alt: "QR code de configuration de l'authenticator"
20
+ authenticator_manual_key: "Ou saisissez cette cl\u00E9 manuellement dans votre application authenticator"
21
+ connecting: "Connexion en cours..."
22
+ back_to_account: "Retour \u00E0 votre compte"
23
+ network_offline: "Vous \u00EAtes hors ligne"
24
+ network_online: "Vous \u00EAtes en ligne"
25
+ toggle_sidebar: "Afficher/masquer la barre lat\u00E9rale"
26
+ skip_to_content: "Aller au contenu principal"
27
+ copied: "Copi\u00E9 dans le presse-papiers"
5
28
  account: Compte
6
29
  signout: Déconnexion
7
30
  signin: Connexion
@@ -173,8 +196,14 @@ fr:
173
196
  inclusion: non acceptées
174
197
  web3_address:
175
198
  not_correct: incorrect
199
+ lato/operation:
200
+ attributes:
201
+ base:
202
+ start_error: "Impossible de lancer le job"
176
203
  lato/invitation:
177
204
  attributes:
205
+ email:
206
+ taken: "d\u00E9j\u00E0 utilis\u00E9e par un autre utilisateur"
178
207
  base:
179
208
  already_accepted: Invitation déjà acceptée par l'utilisateur
180
209
  email_sending_error: Impossible d'envoyer l'e-mail
@@ -2,6 +2,29 @@
2
2
 
3
3
  it:
4
4
  lato:
5
+ close: "Chiudi"
6
+ notice_title: "Notifica"
7
+ alert_title: "Errore"
8
+ sort_ascending: "Ordina per %{column} crescente"
9
+ sort_descending: "Ordina per %{column} decrescente"
10
+ password_requirements: "Almeno %{count} caratteri."
11
+ too_many_requests: "Troppe richieste, attendi %{minutes} minuti prima di riprovare."
12
+ hcaptcha_missing: "Completa la verifica captcha."
13
+ hcaptcha_failed: "Verifica captcha non riuscita, riprova."
14
+ security_confirmation_required: "Conferma la password attuale e il secondo fattore attivo per continuare."
15
+ current_password: "Password attuale"
16
+ current_authenticator_code: "Codice authenticator attuale"
17
+ security_key_confirmation_help: "Conferma con la tua chiave di sicurezza registrata al momento dell'invio."
18
+ confirm_authenticator_code: "Conferma codice authenticator"
19
+ authenticator_qr_code_alt: "Codice QR per configurare l'authenticator"
20
+ authenticator_manual_key: "Oppure inserisci manualmente questa chiave nella tua app authenticator"
21
+ connecting: "Connessione in corso..."
22
+ back_to_account: "Torna al tuo account"
23
+ network_offline: "Sei offline"
24
+ network_online: "Sei online"
25
+ toggle_sidebar: "Mostra/nascondi barra laterale"
26
+ skip_to_content: "Vai al contenuto principale"
27
+ copied: "Copiato negli appunti"
5
28
  account: Account
6
29
  signout: Esci
7
30
  signin: Accedi
@@ -173,8 +196,14 @@ it:
173
196
  inclusion: non accettati
174
197
  web3_address:
175
198
  not_correct: non corretto
199
+ lato/operation:
200
+ attributes:
201
+ base:
202
+ start_error: "Impossibile eseguire il job"
176
203
  lato/invitation:
177
204
  attributes:
205
+ email:
206
+ taken: "gi\u00E0 utilizzata da un altro utente"
178
207
  base:
179
208
  already_accepted: Invito già accettato dall'utente
180
209
  email_sending_error: Impossibile inviare mail