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.
- checksums.yaml +4 -4
- data/AGENTS.md +149 -0
- data/CHANGELOG.md +40 -0
- data/README.md +17 -0
- data/app/controllers/concerns/i18n_proofreading/request_context.rb +41 -0
- data/app/controllers/i18n_proofreading/application_controller.rb +9 -29
- data/app/controllers/i18n_proofreading/dashboard_controller.rb +30 -0
- data/app/controllers/i18n_proofreading/submissions_controller.rb +74 -0
- data/app/controllers/i18n_proofreading/suggestions_controller.rb +1 -64
- data/app/views/i18n_proofreading/shared/_dashboard.html.erb +13 -0
- data/app/views/i18n_proofreading/suggestions/index.html.erb +88 -86
- data/app/views/i18n_proofreading/suggestions/show.html.erb +7 -5
- data/app/views/layouts/i18n_proofreading/application.html.erb +8 -5
- data/config/routes.rb +7 -3
- data/lib/generators/i18n_proofreading/install/install_generator.rb +3 -6
- data/lib/generators/i18n_proofreading/install/templates/create_i18n_proofreading_suggestions.rb.tt +1 -1
- data/lib/generators/i18n_proofreading/install/templates/initializer.rb +8 -0
- data/lib/generators/i18n_proofreading/migration_helpers.rb +28 -0
- data/lib/i18n_proofreading/configuration.rb +25 -3
- data/lib/i18n_proofreading/dashboard.css +153 -156
- data/lib/i18n_proofreading/engine.rb +4 -0
- data/lib/i18n_proofreading/seeds.rb +63 -0
- data/lib/i18n_proofreading/version.rb +1 -1
- data/lib/i18n_proofreading.rb +9 -0
- data/lib/tasks/i18n_proofreading_tasks.rake +9 -0
- metadata +9 -1
|
@@ -1,99 +1,101 @@
|
|
|
1
1
|
<% content_for :body_class, 'ip-index' %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
</nav>
|
|
19
|
+
</nav>
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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="
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
<div class="empty"><%= t('i18n_proofreading.dashboard.empty', default: 'Nothing here yet.') %></div>
|
|
65
|
-
<% end %>
|
|
66
|
-
</div>
|
|
67
|
+
</div>
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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="
|
|
14
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
data/lib/generators/i18n_proofreading/install/templates/create_i18n_proofreading_suggestions.rb.tt
CHANGED
|
@@ -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
|
|
18
|
-
#
|
|
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 =
|
|
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) }
|