i18n_proofreading 0.10.5 → 0.11.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.
@@ -1,99 +1,101 @@
1
1
  <% content_for :body_class, 'ip-index' %>
2
2
 
3
- <header class="page">
4
- <h1><%= t('i18n_proofreading.dashboard.title', default: 'I18nProofreading') %></h1>
5
- <p class="lede"><%= t('i18n_proofreading.dashboard.subtitle', default: 'Review what reviewers proposed, then apply it to your locale files.') %></p>
6
- </header>
3
+ <%= render layout: 'i18n_proofreading/shared/dashboard' do %>
4
+ <header class="page">
5
+ <h1><%= t('i18n_proofreading.dashboard.title', default: 'I18nProofreading') %></h1>
6
+ <p class="lede"><%= t('i18n_proofreading.dashboard.subtitle', default: 'Review what reviewers proposed, then apply it to your locale files.') %></p>
7
+ </header>
7
8
 
8
- <div class="review-shell <%= 'has-selected' if @selected_suggestion %>">
9
- <aside class="review-sidebar" aria-label="<%= t('i18n_proofreading.dashboard.suggestions', default: 'Suggestions') %>">
10
- <nav class="tabs">
11
- <% I18nProofreading::Suggestion::STATUSES.each do |status| %>
12
- <%= link_to suggestions_path(status: status, locale: @locale),
13
- class: ('active' if status == @status) do %>
14
- <%= t("i18n_proofreading.statuses.#{status}", default: status.humanize) %>
15
- <span class="count"><%= @counts.fetch(status, 0) %></span>
9
+ <div class="review-shell <%= 'has-selected' if @selected_suggestion %>">
10
+ <aside class="review-sidebar" aria-label="<%= t('i18n_proofreading.dashboard.suggestions', default: 'Suggestions') %>">
11
+ <nav class="tabs">
12
+ <% I18nProofreading::Suggestion::STATUSES.each do |status| %>
13
+ <%= link_to suggestions_path(status: status, locale: @locale),
14
+ class: ('active' if status == @status) do %>
15
+ <%= t("i18n_proofreading.statuses.#{status}", default: status.humanize) %>
16
+ <span class="count"><%= @counts.fetch(status, 0) %></span>
17
+ <% end %>
16
18
  <% end %>
17
- <% end %>
18
- </nav>
19
+ </nav>
19
20
 
20
- <% if @locales.many? %>
21
- <form class="filters" method="get">
22
- <input type="hidden" name="status" value="<%= @status %>">
23
- <label for="i18np-locale"><%= t('i18n_proofreading.dashboard.locale', default: 'Locale') %></label>
24
- <select id="i18np-locale" name="locale" onchange="this.form.submit()">
25
- <option value=""><%= t('i18n_proofreading.dashboard.all_locales', default: 'All locales') %></option>
26
- <% @locales.each do |locale| %>
27
- <option value="<%= locale %>" <%= 'selected' if locale == @locale %>><%= locale %></option>
28
- <% end %>
29
- </select>
30
- <%# Inline onchange auto-submits where allowed; this button is the reliable
31
- path where a strict CSP blocks inline handlers, or with JS off. %>
32
- <button><%= t('i18n_proofreading.dashboard.filter', default: 'Filter') %></button>
33
- </form>
34
- <% end %>
21
+ <% if @locales.many? %>
22
+ <form class="filters" method="get">
23
+ <input type="hidden" name="status" value="<%= @status %>">
24
+ <label for="i18np-locale"><%= t('i18n_proofreading.dashboard.locale', default: 'Locale') %></label>
25
+ <select id="i18np-locale" name="locale" onchange="this.form.submit()">
26
+ <option value=""><%= t('i18n_proofreading.dashboard.all_locales', default: 'All locales') %></option>
27
+ <% @locales.each do |locale| %>
28
+ <option value="<%= locale %>" <%= 'selected' if locale == @locale %>><%= locale %></option>
29
+ <% end %>
30
+ </select>
31
+ <%# Inline onchange auto-submits where allowed; this button is the reliable
32
+ path where a strict CSP blocks inline handlers, or with JS off. %>
33
+ <button><%= t('i18n_proofreading.dashboard.filter', default: 'Filter') %></button>
34
+ </form>
35
+ <% end %>
35
36
 
36
- <div class="suggestion-list">
37
- <% if @suggestions.any? %>
38
- <% @suggestions.each do |suggestion| %>
39
- <%= link_to suggestions_path(status: @status, locale: @locale, page: @page,
40
- suggestion_id: suggestion.id),
41
- class: ['suggestion-row', ('active' if @selected_suggestion&.id == suggestion.id)].compact do %>
42
- <span class="suggestion-main">
43
- <span class="suggestion-topline">
44
- <code class="key"><%= suggestion.translation_key %></code>
45
- <span class="badge locale"><%= suggestion.locale %></span>
46
- <span class="muted suggestion-time" title="<%= suggestion.created_at %>">
47
- <%= time_ago_in_words(suggestion.created_at) %> <%= t('i18n_proofreading.dashboard.ago', default: 'ago') %>
37
+ <div class="suggestion-list">
38
+ <% if @suggestions.any? %>
39
+ <% @suggestions.each do |suggestion| %>
40
+ <%= link_to suggestions_path(status: @status, locale: @locale, page: @page,
41
+ suggestion_id: suggestion.id),
42
+ class: ['suggestion-row', ('active' if @selected_suggestion&.id == suggestion.id)].compact do %>
43
+ <span class="suggestion-main">
44
+ <span class="suggestion-topline">
45
+ <code class="key"><%= suggestion.translation_key %></code>
46
+ <span class="badge locale"><%= suggestion.locale %></span>
47
+ <span class="muted suggestion-time" title="<%= suggestion.created_at %>">
48
+ <%= time_ago_in_words(suggestion.created_at) %> <%= t('i18n_proofreading.dashboard.ago', default: 'ago') %>
49
+ </span>
50
+ </span>
51
+ <span class="suggestion-copy" dir="auto"><%= truncate(suggestion.proposed_value, length: 110) %></span>
52
+ <span class="muted suggestion-meta">
53
+ <span>#<%= suggestion.id %></span>
54
+ <% if suggestion.author_label.present? %>
55
+ <span><%= suggestion.author_label %></span>
56
+ <% end %>
48
57
  </span>
49
58
  </span>
50
- <span class="suggestion-copy" dir="auto"><%= truncate(suggestion.proposed_value, length: 110) %></span>
51
- <span class="muted suggestion-meta">
52
- <span>#<%= suggestion.id %></span>
53
- <% if suggestion.author_label.present? %>
54
- <span><%= suggestion.author_label %></span>
55
- <% end %>
59
+ <span class="badge status-<%= suggestion.status %>">
60
+ <%= t("i18n_proofreading.statuses.#{suggestion.status}", default: suggestion.status.humanize) %>
56
61
  </span>
57
- </span>
58
- <span class="badge status-<%= suggestion.status %>">
59
- <%= t("i18n_proofreading.statuses.#{suggestion.status}", default: suggestion.status.humanize) %>
60
- </span>
62
+ <% end %>
61
63
  <% end %>
64
+ <% else %>
65
+ <div class="empty"><%= t('i18n_proofreading.dashboard.empty', default: 'Nothing here yet.') %></div>
62
66
  <% end %>
63
- <% else %>
64
- <div class="empty"><%= t('i18n_proofreading.dashboard.empty', default: 'Nothing here yet.') %></div>
65
- <% end %>
66
- </div>
67
+ </div>
67
68
 
68
- <nav class="pager">
69
- <span>
70
- <% if @page > 1 %>
71
- <%= link_to t('i18n_proofreading.dashboard.newer', default: '← Newer'),
72
- suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
73
- <% end %>
74
- </span>
75
- <span>
76
- <% if @more %>
77
- <%= link_to t('i18n_proofreading.dashboard.older', default: 'Older →'),
78
- suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
79
- <% end %>
80
- </span>
81
- </nav>
82
- </aside>
69
+ <nav class="pager">
70
+ <span>
71
+ <% if @page > 1 %>
72
+ <%= link_to t('i18n_proofreading.dashboard.newer', default: '← Newer'),
73
+ suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
74
+ <% end %>
75
+ </span>
76
+ <span>
77
+ <% if @more %>
78
+ <%= link_to t('i18n_proofreading.dashboard.older', default: 'Older →'),
79
+ suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
80
+ <% end %>
81
+ </span>
82
+ </nav>
83
+ </aside>
83
84
 
84
- <main class="review-detail" aria-label="<%= t('i18n_proofreading.dashboard.current_suggestion', default: 'Current suggestion') %>">
85
- <% if @selected_suggestion %>
86
- <p class="mobile-back">
87
- <%= link_to t('i18n_proofreading.dashboard.back', default: '← All suggestions'),
88
- suggestions_path(status: @status, locale: @locale, page: @page) %>
89
- </p>
90
- <%= render 'suggestion_panel', suggestion: @selected_suggestion %>
91
- <% else %>
92
- <div class="empty-state">
93
- <h2><%= t('i18n_proofreading.dashboard.select_suggestion', default: 'Select a suggestion') %></h2>
94
- <p class="muted"><%= t('i18n_proofreading.dashboard.select_suggestion_hint',
95
- default: 'Open a row to review the proposed wording beside the queue.') %></p>
96
- </div>
97
- <% end %>
98
- </main>
99
- </div>
85
+ <main class="review-detail" aria-label="<%= t('i18n_proofreading.dashboard.current_suggestion', default: 'Current suggestion') %>">
86
+ <% if @selected_suggestion %>
87
+ <p class="mobile-back">
88
+ <%= link_to t('i18n_proofreading.dashboard.back', default: '← All suggestions'),
89
+ suggestions_path(status: @status, locale: @locale, page: @page) %>
90
+ </p>
91
+ <%= render 'suggestion_panel', suggestion: @selected_suggestion %>
92
+ <% else %>
93
+ <div class="empty-state">
94
+ <h2><%= t('i18n_proofreading.dashboard.select_suggestion', default: 'Select a suggestion') %></h2>
95
+ <p class="muted"><%= t('i18n_proofreading.dashboard.select_suggestion_hint',
96
+ default: 'Open a row to review the proposed wording beside the queue.') %></p>
97
+ </div>
98
+ <% end %>
99
+ </main>
100
+ </div>
101
+ <% end %>
@@ -1,8 +1,10 @@
1
1
  <% content_for :body_class, 'ip-show' %>
2
2
 
3
- <p class="breadcrumb">
4
- <%= link_to t('i18n_proofreading.dashboard.title', default: 'I18nProofreading'), suggestions_path %>
5
- / #<%= @suggestion.id %>
6
- </p>
3
+ <%= render layout: 'i18n_proofreading/shared/dashboard' do %>
4
+ <p class="breadcrumb">
5
+ <%= link_to t('i18n_proofreading.dashboard.title', default: 'I18nProofreading'), suggestions_path %>
6
+ / #<%= @suggestion.id %>
7
+ </p>
7
8
 
8
- <%= render 'suggestion_panel', suggestion: @suggestion %>
9
+ <%= render 'suggestion_panel', suggestion: @suggestion %>
10
+ <% end %>
@@ -6,12 +6,15 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
- <%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{I18nProofreading::Widget.dashboard_stylesheet_fingerprint}",
10
- 'data-turbo-track': 'reload' %>
9
+ <%# The dashboard's stylesheet is declared by the views
10
+ (i18n_proofreading/shared/_dashboard), not here, so it survives a
11
+ host replacing this layout via config.admin_layout or config.base_controller_class. %>
11
12
  </head>
12
- <body class="<%= content_for?(:body_class) ? yield(:body_class) : '' %>">
13
- <div class="container">
14
- <%= yield %>
13
+ <body class="ip-page <%= content_for?(:body_class) ? yield(:body_class) : '' %>">
14
+ <div class="ip-dashboard">
15
+ <div class="container">
16
+ <%= yield %>
17
+ </div>
15
18
  </div>
16
19
  </body>
17
20
  </html>
data/config/routes.rb CHANGED
@@ -10,9 +10,13 @@ I18nProofreading::Engine.routes.draw do
10
10
  # route are the widget's public API (see SuggestionsController). There is
11
11
  # deliberately no update/destroy — the tool never mutates suggestions from the
12
12
  # UI, since it can't write to the host's locale files.
13
- resources :suggestions, only: %i[index show create] do
14
- get :context, on: :collection
15
- end
13
+ # The widget's two endpoints go to SubmissionsController: they are public and
14
+ # the dashboard is staff-only, and only the staff half inherits
15
+ # config.base_controller_class. Sharing one controller would put a host's admin
16
+ # authentication in front of every proofreader. URLs are unchanged.
17
+ get 'suggestions/context', to: 'submissions#context', as: :context_suggestions
18
+ post 'suggestions', to: 'submissions#create', as: :submissions
19
+ resources :suggestions, only: %i[index show]
16
20
 
17
21
  root to: 'suggestions#index'
18
22
  end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require 'rails/generators'
4
4
  require 'rails/generators/active_record'
5
+ require_relative '../migration_helpers'
5
6
 
6
7
  module I18nProofreading
7
8
  module Generators
8
9
  class InstallGenerator < Rails::Generators::Base
9
10
  include ActiveRecord::Generators::Migration
11
+ include MigrationHelpers
10
12
 
11
13
  source_root File.expand_path('templates', __dir__)
12
14
 
@@ -27,14 +29,9 @@ module I18nProofreading
27
29
 
28
30
  def post_install
29
31
  say "\ni18n_proofreading installed. Run `rails db:migrate`, then boot in development", :green
32
+ say 'Optional: run `bin/rails i18n_proofreading:seed_demo` for sample suggestions.'
30
33
  say "and look for the “Suggest edits” pill in the bottom-left corner.\n"
31
34
  end
32
-
33
- private
34
-
35
- def migration_version
36
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
37
- end
38
35
  end
39
36
  end
40
37
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateI18nProofreadingSuggestions < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
- create_table :i18n_proofreading_suggestions do |t|
5
+ create_table :i18n_proofreading_suggestions<%= primary_key_type_option %> do |t|
6
6
  t.string :translation_key, null: false
7
7
  t.string :locale, null: false
8
8
  t.text :old_value
@@ -20,6 +20,14 @@ I18nProofreading.configure do |config|
20
20
  # Render the dashboard inside your app's admin layout. Default: the gem's
21
21
  # standalone dashboard layout.
22
22
  #
23
+ # Two ways to put the dashboard inside an admin you already have.
24
+ #
25
+ # The whole stack — your layout, helpers, authentication and any request
26
+ # context your before_actions set up. Only the dashboard inherits it; the
27
+ # widget's endpoints stay public, so this cannot gate a proofreader.
28
+ # config.base_controller_class = "Admin::BaseController"
29
+ #
30
+ # Or the layout alone:
23
31
  # config.admin_layout = "admin/application"
24
32
 
25
33
  # Attribute a suggestion to a user (optional). Return an object responding to
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ module Generators
5
+ # Shared bits every migration-writing generator needs.
6
+ module MigrationHelpers
7
+ private
8
+
9
+ def migration_version
10
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
11
+ end
12
+
13
+ # Follow the host's own key type instead of forcing bigint, the same
14
+ # `Rails.configuration.generators` lookup Rails' own Active Storage,
15
+ # Action Text and Action Mailbox migrations do — so a host that set it
16
+ # once gets consistent tables from all of them.
17
+ #
18
+ # Rendered as a `create_table` option rather than a bare value, because a
19
+ # template is expanded at generate time: emitting the method name would
20
+ # put `id: primary_key_type` in the migration, where nothing defines it.
21
+ def primary_key_type_option
22
+ config = Rails.configuration.generators
23
+ type = config.options[config.orm][:primary_key_type]
24
+ type ? ", id: :#{type}" : ''
25
+ end
26
+ end
27
+ end
28
+ end
@@ -5,6 +5,10 @@ module I18nProofreading
5
5
  # with zero configuration in development; the hooks below let an app decide who
6
6
  # sees the tool and how suggestions are attributed.
7
7
  class Configuration
8
+ # The gem's own dashboard layout. Compared against, so DashboardController
9
+ # can tell "the host left this alone" from "the host chose this".
10
+ DEFAULT_ADMIN_LAYOUT = 'i18n_proofreading/application'
11
+
8
12
  # Environments the tool is active in. It is never active anywhere else, so a
9
13
  # production deploy can't accidentally expose the key markers or the endpoint.
10
14
  attr_accessor :enabled_environments
@@ -14,8 +18,10 @@ module I18nProofreading
14
18
  # feature flag, an allowlist, etc.
15
19
  attr_accessor :enabled
16
20
 
17
- # Per-request gate for the triage dashboard (browse suggestions, change their
18
- # status, delete). Independent of `enabled` and `enabled_environments`: the
21
+ # Per-request gate for the triage dashboard, which is read-only — browse and
22
+ # read suggestions; there is deliberately no update or destroy, since the
23
+ # tool never writes to the host's locale files.
24
+ # Independent of `enabled` and `enabled_environments`: the
19
25
  # widget is dev/staging-only, but a maintainer may want to triage from
20
26
  # production. Defaults to development only, so a fresh install never exposes
21
27
  # the dashboard until you wire it to your own admin check.
@@ -25,6 +31,21 @@ module I18nProofreading
25
31
  # your app's admin shell, e.g. "admin/application".
26
32
  attr_accessor :admin_layout
27
33
 
34
+ # The controller the DASHBOARD inherits from, as a String so it resolves
35
+ # lazily rather than at config time. Default: a plain
36
+ # 'ActionController::Base', where `authorize_admin` is the only gate.
37
+ #
38
+ # Name the controller your own admin already inherits from and the dashboard
39
+ # adopts that whole stack — layout, helpers, authentication, and any request
40
+ # context your before_actions set up. `admin_layout` covers only the layout,
41
+ # which leaves a host layout calling its own helpers to raise NameError under
42
+ # the engine's isolated namespace.
43
+ #
44
+ # Only the dashboard uses it. The widget's endpoints stay on the engine's own
45
+ # public controller, so an admin base controller here can never demand a
46
+ # staff session from someone suggesting a translation.
47
+ attr_accessor :base_controller_class
48
+
28
49
  # Resolve the current user for attribution (optional). Return an object
29
50
  # responding to #id, or nil. Receives the Rack::Request.
30
51
  attr_accessor :current_user
@@ -72,7 +93,8 @@ module I18nProofreading
72
93
  @enabled_environments = %w[development staging]
73
94
  @enabled = ->(_request) { true }
74
95
  @authorize_admin = ->(_request) { Rails.env.development? }
75
- @admin_layout = 'i18n_proofreading/application'
96
+ @admin_layout = DEFAULT_ADMIN_LAYOUT
97
+ @base_controller_class = 'ActionController::Base'
76
98
  @current_user = ->(_request) {}
77
99
  @author_label = ->(user) { user.respond_to?(:email) ? user.email : user&.to_s }
78
100
  @available_locales = -> { I18n.available_locales.map(&:to_s) }