i18n_feedback 0.7.1 → 0.8.1
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/CHANGELOG.md +29 -0
- data/README.md +32 -1
- data/app/controllers/i18n_feedback/application_controller.rb +13 -4
- data/app/controllers/i18n_feedback/suggestions_controller.rb +27 -1
- data/app/views/i18n_feedback/suggestions/index.html.erb +75 -0
- data/app/views/layouts/i18n_feedback/application.html.erb +95 -0
- data/config/locales/i18n_feedback.en.yml +18 -0
- data/config/routes.rb +9 -1
- data/lib/generators/i18n_feedback/install/templates/initializer.rb +7 -0
- data/lib/i18n_feedback/configuration.rb +8 -0
- data/lib/i18n_feedback/version.rb +1 -1
- data/lib/i18n_feedback/widget.js +1 -1
- data/lib/i18n_feedback.rb +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7961f050a8f18c3136f8bac4be58014c6d137b408539d9bbb589e3756c024674
|
|
4
|
+
data.tar.gz: eb772bb6ca0dbd76d2638f6d5de2fb455a3c1ac737b640856fff79677f50f873
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a586da5d40cda89a58312e02bdeaaa27af64bc9c7013fca26cbd94bf7dd01da8109dfb2ce7417bf07fae3525837740d85441a6e47e3c65cebcb85dfe4125c53f
|
|
7
|
+
data.tar.gz: 1978b54ebb78dadfbc90d09e97d4a8e656c2f77d34d134301e0844efbcf3848c13d95f8b2ea5311ef8f872f21439852c6c67564c981b3b42d08f2eee085376bc
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.1]
|
|
6
|
+
|
|
7
|
+
- Make the dashboard **read-only**. Apply / Reject / Reopen / Delete only ever
|
|
8
|
+
flipped the stored `status` — they never touched the host's locale files, so
|
|
9
|
+
offering them implied a capability the gem doesn't have. The buttons and their
|
|
10
|
+
`update` / `destroy` routes and actions are removed; the dashboard now just
|
|
11
|
+
lists suggestions for review, and you make the edits in your own locale files.
|
|
12
|
+
A suggestion's `status` still exists on the model for out-of-band tracking and
|
|
13
|
+
as groundwork for a future "apply to locale file" feature.
|
|
14
|
+
|
|
15
|
+
## [0.8.0]
|
|
16
|
+
|
|
17
|
+
- Add a built-in **triage dashboard**, mounted at the engine root (your
|
|
18
|
+
`mount_path`, default `/i18n_feedback`): pending / applied / rejected tabs
|
|
19
|
+
with counts, a per-locale filter, each suggestion shown current-vs-proposed,
|
|
20
|
+
and one-click Apply / Reject / Reopen / Delete. Plain server-rendered HTML
|
|
21
|
+
with its own self-contained styling (light + dark via `prefers-color-scheme`)
|
|
22
|
+
— no host assets or JS framework required.
|
|
23
|
+
- Every dashboard string renders through Rails I18n under `i18n_feedback.dashboard.*`
|
|
24
|
+
and `i18n_feedback.statuses.*`, with English fallbacks, so a host can translate
|
|
25
|
+
or reword any of it from its own locale files.
|
|
26
|
+
- Add `config.authorize_admin` — the dashboard's gate, **defaulting to
|
|
27
|
+
development only** so a fresh install never exposes it in production. It is
|
|
28
|
+
independent of `enabled` / `enabled_environments`, so a maintainer can triage
|
|
29
|
+
from production even where the widget is off.
|
|
30
|
+
- The widget's "already suggested" context now loads from
|
|
31
|
+
`GET {mount_path}/suggestions/context` (was the collection index, now the
|
|
32
|
+
dashboard). Internal to the gem; the bundled widget was updated in lockstep.
|
|
33
|
+
|
|
5
34
|
## [0.7.1]
|
|
6
35
|
|
|
7
36
|
- The `i18n_feedback.stop` toggle label now reads "Stop suggesting (Esc)" in
|
data/README.md
CHANGED
|
@@ -97,6 +97,10 @@ I18nFeedback.configure do |config|
|
|
|
97
97
|
# Extra per-request gate. Return false to hide the tool. Receives the request.
|
|
98
98
|
config.enabled = ->(request) { true }
|
|
99
99
|
|
|
100
|
+
# Who may open the triage dashboard. Independent of the gates above; defaults
|
|
101
|
+
# to development only. Wire it to your own admin check to open it elsewhere.
|
|
102
|
+
config.authorize_admin = ->(request) { Rails.env.development? }
|
|
103
|
+
|
|
100
104
|
# Attribute a suggestion to a user (optional). Return an object responding to
|
|
101
105
|
# #id, or nil. Receives the request.
|
|
102
106
|
config.current_user = ->(request) { nil }
|
|
@@ -233,7 +237,34 @@ blue accent stays the same in both.
|
|
|
233
237
|
|
|
234
238
|
## Reviewing suggestions
|
|
235
239
|
|
|
236
|
-
|
|
240
|
+
### Review dashboard
|
|
241
|
+
|
|
242
|
+
Mounted at your `mount_path` (default `/i18n_feedback`), the engine root is a
|
|
243
|
+
built-in **read-only** review board: pending / applied / rejected tabs with
|
|
244
|
+
counts, a per-locale filter, and each suggestion shown as current-vs-proposed
|
|
245
|
+
with its comment and author. It's plain server-rendered HTML with its own
|
|
246
|
+
styling — no host assets or JS framework needed.
|
|
247
|
+
|
|
248
|
+
It is deliberately read-only. The gem never writes to your locale files, so it
|
|
249
|
+
doesn't pretend to: you review the suggestions here, then make the edits in your
|
|
250
|
+
own `config/locales/*.yml`. (A suggestion's `status` still exists on the model
|
|
251
|
+
for your own tracking — set it from the console — and is the groundwork for a
|
|
252
|
+
future "apply to locale file" feature.)
|
|
253
|
+
|
|
254
|
+
It has its own gate, `config.authorize_admin`, **defaulting to development
|
|
255
|
+
only** — so a fresh install never exposes it in production. Point it at your own
|
|
256
|
+
admin check to open it elsewhere:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The gate is independent of `enabled` / `enabled_environments`: the widget can be
|
|
263
|
+
dev/staging-only while a maintainer still triages from production.
|
|
264
|
+
|
|
265
|
+
### From the console
|
|
266
|
+
|
|
267
|
+
Suggestions are also ordinary records:
|
|
237
268
|
|
|
238
269
|
```ruby
|
|
239
270
|
I18nFeedback::Suggestion.where(status: "pending").newest_first.each do |s|
|
|
@@ -4,16 +4,25 @@ module I18nFeedback
|
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
5
|
protect_from_forgery with: :exception
|
|
6
6
|
|
|
7
|
-
before_action :require_available
|
|
8
|
-
|
|
9
7
|
private
|
|
10
8
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# Gate for the widget's public API (submit a suggestion, read prior context).
|
|
10
|
+
# The client can set the cookie, but it can never reach the endpoints unless
|
|
11
|
+
# the app itself says the tool is available for this request.
|
|
13
12
|
def require_available
|
|
14
13
|
head :forbidden unless I18nFeedback.available?(request)
|
|
15
14
|
end
|
|
16
15
|
|
|
16
|
+
# Gate for the triage dashboard. Independent of #require_available (see
|
|
17
|
+
# I18nFeedback.admin?). Renders a plain 403 with a hint rather than a bare
|
|
18
|
+
# head, since a human is usually looking at it.
|
|
19
|
+
def require_admin
|
|
20
|
+
return if I18nFeedback.admin?(request)
|
|
21
|
+
|
|
22
|
+
render plain: 'Forbidden. Set I18nFeedback.config.authorize_admin to grant access.',
|
|
23
|
+
status: :forbidden
|
|
24
|
+
end
|
|
25
|
+
|
|
17
26
|
def current_author
|
|
18
27
|
return @current_author if defined?(@current_author)
|
|
19
28
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module I18nFeedback
|
|
4
4
|
class SuggestionsController < ApplicationController
|
|
5
|
+
PER_PAGE = 50
|
|
6
|
+
|
|
7
|
+
# Public widget API is available-gated; the read-only dashboard is admin-gated.
|
|
8
|
+
before_action :require_available, only: %i[context create]
|
|
9
|
+
before_action :require_admin, only: :index
|
|
10
|
+
|
|
11
|
+
layout 'i18n_feedback/application', only: :index
|
|
12
|
+
|
|
5
13
|
# Throttle the public submission endpoint per IP so one user or bot can't
|
|
6
14
|
# flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
|
|
7
15
|
# Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
|
|
@@ -15,9 +23,27 @@ module I18nFeedback
|
|
|
15
23
|
})
|
|
16
24
|
end
|
|
17
25
|
|
|
26
|
+
# --- triage dashboard (admin) --------------------------------------------
|
|
27
|
+
|
|
28
|
+
def index
|
|
29
|
+
@status = Suggestion::STATUSES.include?(params[:status]) ? params[:status] : 'pending'
|
|
30
|
+
@locale = params[:locale].presence
|
|
31
|
+
@counts = Suggestion.group(:status).count
|
|
32
|
+
@locales = Suggestion.distinct.pluck(:locale).compact.sort
|
|
33
|
+
|
|
34
|
+
scope = Suggestion.where(status: @status)
|
|
35
|
+
scope = scope.where(locale: @locale) if @locale
|
|
36
|
+
@page = [params[:page].to_i, 1].max
|
|
37
|
+
rows = scope.newest_first.offset((@page - 1) * PER_PAGE).limit(PER_PAGE + 1).to_a
|
|
38
|
+
@more = rows.size > PER_PAGE
|
|
39
|
+
@suggestions = rows.first(PER_PAGE)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- widget API (public) -------------------------------------------------
|
|
43
|
+
|
|
18
44
|
# Pending suggestions for one key/locale, shown as read-only context when the
|
|
19
45
|
# proofreader reopens the popover for a string someone already flagged.
|
|
20
|
-
def
|
|
46
|
+
def context
|
|
21
47
|
suggestions = Suggestion
|
|
22
48
|
.where(translation_key: params[:key], locale: params[:locale])
|
|
23
49
|
.status_pending
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<header class="page">
|
|
2
|
+
<h1><%= t('i18n_feedback.dashboard.title', default: 'Translation suggestions') %></h1>
|
|
3
|
+
<p class="lede"><%= t('i18n_feedback.dashboard.subtitle', default: 'Review what reviewers proposed, then apply it to your locale files.') %></p>
|
|
4
|
+
</header>
|
|
5
|
+
|
|
6
|
+
<nav class="tabs">
|
|
7
|
+
<% I18nFeedback::Suggestion::STATUSES.each do |status| %>
|
|
8
|
+
<%= link_to suggestions_path(status: status, locale: @locale),
|
|
9
|
+
class: ('active' if status == @status) do %>
|
|
10
|
+
<%= t("i18n_feedback.statuses.#{status}", default: status.humanize) %>
|
|
11
|
+
<span class="count"><%= @counts.fetch(status, 0) %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</nav>
|
|
15
|
+
|
|
16
|
+
<% if @locales.many? %>
|
|
17
|
+
<form class="filters" method="get">
|
|
18
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
19
|
+
<select name="locale" onchange="this.form.requestSubmit()"
|
|
20
|
+
aria-label="<%= t('i18n_feedback.dashboard.filter', default: 'Filter by locale') %>">
|
|
21
|
+
<option value=""><%= t('i18n_feedback.dashboard.all_locales', default: 'All locales') %></option>
|
|
22
|
+
<% @locales.each do |locale| %>
|
|
23
|
+
<option value="<%= locale %>" <%= 'selected' if locale == @locale %>><%= locale %></option>
|
|
24
|
+
<% end %>
|
|
25
|
+
</select>
|
|
26
|
+
<noscript><button><%= t('i18n_feedback.dashboard.filter', default: 'Filter') %></button></noscript>
|
|
27
|
+
</form>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<% if @suggestions.any? %>
|
|
31
|
+
<% @suggestions.each do |suggestion| %>
|
|
32
|
+
<article class="card">
|
|
33
|
+
<div class="row-top">
|
|
34
|
+
<code class="key"><%= suggestion.translation_key %></code>
|
|
35
|
+
<span class="badge locale"><%= suggestion.locale %></span>
|
|
36
|
+
<span class="badge status-<%= suggestion.status %>">
|
|
37
|
+
<%= t("i18n_feedback.statuses.#{suggestion.status}", default: suggestion.status.humanize) %>
|
|
38
|
+
</span>
|
|
39
|
+
<span class="spacer"></span>
|
|
40
|
+
<span class="meta">
|
|
41
|
+
<% if suggestion.author_label.present? %><%= suggestion.author_label %> · <% end %>
|
|
42
|
+
<span title="<%= suggestion.created_at %>"><%= time_ago_in_words(suggestion.created_at) %> <%= t('i18n_feedback.dashboard.ago', default: 'ago') %></span>
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<dl class="diff">
|
|
47
|
+
<dt><%= t('i18n_feedback.dashboard.current', default: 'Current') %></dt>
|
|
48
|
+
<dd><span class="old" dir="auto"><%= suggestion.old_value.presence || '—' %></span></dd>
|
|
49
|
+
<dt><%= t('i18n_feedback.dashboard.suggested', default: 'Suggested') %></dt>
|
|
50
|
+
<dd><span class="new" dir="auto"><%= suggestion.proposed_value %></span></dd>
|
|
51
|
+
</dl>
|
|
52
|
+
|
|
53
|
+
<% if suggestion.comment.present? %>
|
|
54
|
+
<div class="comment" dir="auto"><%= suggestion.comment %></div>
|
|
55
|
+
<% end %>
|
|
56
|
+
</article>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<nav class="pager">
|
|
60
|
+
<span>
|
|
61
|
+
<% if @page > 1 %>
|
|
62
|
+
<%= link_to t('i18n_feedback.dashboard.newer', default: '← Newer'),
|
|
63
|
+
suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
|
|
64
|
+
<% end %>
|
|
65
|
+
</span>
|
|
66
|
+
<span>
|
|
67
|
+
<% if @more %>
|
|
68
|
+
<%= link_to t('i18n_feedback.dashboard.older', default: 'Older →'),
|
|
69
|
+
suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
|
|
70
|
+
<% end %>
|
|
71
|
+
</span>
|
|
72
|
+
</nav>
|
|
73
|
+
<% else %>
|
|
74
|
+
<div class="empty"><%= t('i18n_feedback.dashboard.empty', default: 'Nothing here yet.') %></div>
|
|
75
|
+
<% end %>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title><%= t('i18n_feedback.dashboard.title', default: 'Translation suggestions') %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
12
|
+
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff; --code: #f0f1f3;
|
|
13
|
+
--pending: #d97706; --applied: #16a34a; --rejected: #6b7280;
|
|
14
|
+
}
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
18
|
+
--border: #2a313a; --accent: #3b82f6; --code: #232a33;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
margin: 0; background: var(--bg); color: var(--text);
|
|
24
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
+
}
|
|
26
|
+
a { color: var(--accent); text-decoration: none; }
|
|
27
|
+
a:hover { text-decoration: underline; }
|
|
28
|
+
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
29
|
+
h1 { font-size: 22px; margin: 0; }
|
|
30
|
+
.lede { color: var(--muted); margin: 4px 0 0; }
|
|
31
|
+
header.page { margin-bottom: 20px; }
|
|
32
|
+
|
|
33
|
+
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
34
|
+
.tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
|
|
35
|
+
.tabs a.active {
|
|
36
|
+
color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
37
|
+
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px;
|
|
38
|
+
}
|
|
39
|
+
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
40
|
+
.count {
|
|
41
|
+
display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
42
|
+
background: var(--border); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
46
|
+
.filters select {
|
|
47
|
+
padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
|
|
48
|
+
background: var(--surface); color: var(--text); font: inherit;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.card {
|
|
52
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
53
|
+
padding: 14px 16px; margin-bottom: 12px;
|
|
54
|
+
}
|
|
55
|
+
.row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
56
|
+
code.key {
|
|
57
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
|
|
58
|
+
background: var(--code); padding: 2px 7px; border-radius: 6px; overflow-wrap: anywhere;
|
|
59
|
+
}
|
|
60
|
+
.badge {
|
|
61
|
+
display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
62
|
+
color: #fff; white-space: nowrap;
|
|
63
|
+
}
|
|
64
|
+
.badge.status-pending { background: var(--pending); }
|
|
65
|
+
.badge.status-applied { background: var(--applied); }
|
|
66
|
+
.badge.status-rejected { background: var(--rejected); }
|
|
67
|
+
.badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
68
|
+
.spacer { flex: 1; }
|
|
69
|
+
.meta { color: var(--muted); font-size: 13px; }
|
|
70
|
+
|
|
71
|
+
dl.diff { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
72
|
+
dl.diff dt { color: var(--muted); font-size: 13px; }
|
|
73
|
+
dl.diff dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
74
|
+
dd .old { color: var(--muted); }
|
|
75
|
+
dd .new { font-weight: 600; }
|
|
76
|
+
.comment { margin-top: 10px; padding: 8px 10px; background: var(--code); border-radius: 8px; font-size: 14px; overflow-wrap: anywhere; }
|
|
77
|
+
|
|
78
|
+
button {
|
|
79
|
+
padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
80
|
+
background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
button:hover { border-color: var(--muted); }
|
|
83
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
84
|
+
|
|
85
|
+
.empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
|
|
86
|
+
.pager { margin-top: 16px; display: flex; justify-content: space-between; }
|
|
87
|
+
.pager .muted { color: var(--muted); }
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div class="container">
|
|
92
|
+
<%= yield %>
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
@@ -14,3 +14,21 @@ en:
|
|
|
14
14
|
save: "Send suggestion"
|
|
15
15
|
error_blank: "Please enter a suggestion."
|
|
16
16
|
error_save: "Could not save the suggestion."
|
|
17
|
+
# Triage dashboard. English-only by default (an admin-facing surface); every
|
|
18
|
+
# string is rendered through I18n with these as fallbacks, so a host can
|
|
19
|
+
# translate or reword any of them under its own `i18n_feedback.*` scope.
|
|
20
|
+
statuses:
|
|
21
|
+
pending: "Pending"
|
|
22
|
+
applied: "Applied"
|
|
23
|
+
rejected: "Rejected"
|
|
24
|
+
dashboard:
|
|
25
|
+
title: "Translation suggestions"
|
|
26
|
+
subtitle: "Review what reviewers proposed, then apply it to your locale files."
|
|
27
|
+
filter: "Filter by locale"
|
|
28
|
+
all_locales: "All locales"
|
|
29
|
+
current: "Current"
|
|
30
|
+
suggested: "Suggested"
|
|
31
|
+
ago: "ago"
|
|
32
|
+
empty: "Nothing here yet."
|
|
33
|
+
newer: "← Newer"
|
|
34
|
+
older: "Older →"
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
I18nFeedback::Engine.routes.draw do
|
|
4
|
-
|
|
4
|
+
# index is the read-only admin dashboard; create + the `context` collection
|
|
5
|
+
# route are the widget's public API (see SuggestionsController). There is
|
|
6
|
+
# deliberately no update/destroy — the tool never mutates suggestions from the
|
|
7
|
+
# UI, since it can't write to the host's locale files.
|
|
8
|
+
resources :suggestions, only: %i[index create] do
|
|
9
|
+
get :context, on: :collection
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
root to: 'suggestions#index'
|
|
5
13
|
end
|
|
@@ -10,6 +10,13 @@ I18nFeedback.configure do |config|
|
|
|
10
10
|
#
|
|
11
11
|
# config.enabled = ->(request) { true }
|
|
12
12
|
|
|
13
|
+
# Who may open the triage dashboard (mounted at your `mount_path`) to review,
|
|
14
|
+
# apply, and delete suggestions. Independent of the checks above — the widget
|
|
15
|
+
# is dev/staging-only, but you may want to triage from production. Defaults to
|
|
16
|
+
# development only; wire it to your own admin check to open it elsewhere.
|
|
17
|
+
#
|
|
18
|
+
# config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
|
|
19
|
+
|
|
13
20
|
# Attribute a suggestion to a user (optional). Return an object responding to
|
|
14
21
|
# #id (ideally #email too), or nil. Receives the Rack::Request. You resolve the
|
|
15
22
|
# user however your app does — session, Warden, a token, etc.
|
|
@@ -14,6 +14,13 @@ module I18nFeedback
|
|
|
14
14
|
# feature flag, an allowlist, etc.
|
|
15
15
|
attr_accessor :enabled
|
|
16
16
|
|
|
17
|
+
# Per-request gate for the triage dashboard (browse suggestions, change their
|
|
18
|
+
# status, delete). Independent of `enabled` and `enabled_environments`: the
|
|
19
|
+
# widget is dev/staging-only, but a maintainer may want to triage from
|
|
20
|
+
# production. Defaults to development only, so a fresh install never exposes
|
|
21
|
+
# the dashboard until you wire it to your own admin check.
|
|
22
|
+
attr_accessor :authorize_admin
|
|
23
|
+
|
|
17
24
|
# Resolve the current user for attribution (optional). Return an object
|
|
18
25
|
# responding to #id, or nil. Receives the Rack::Request.
|
|
19
26
|
attr_accessor :current_user
|
|
@@ -60,6 +67,7 @@ module I18nFeedback
|
|
|
60
67
|
def initialize
|
|
61
68
|
@enabled_environments = %w[development staging]
|
|
62
69
|
@enabled = ->(_request) { true }
|
|
70
|
+
@authorize_admin = ->(_request) { Rails.env.development? }
|
|
63
71
|
@current_user = ->(_request) {}
|
|
64
72
|
@author_label = ->(user) { user.respond_to?(:email) ? user.email : user&.to_s }
|
|
65
73
|
@available_locales = -> { I18n.available_locales.map(&:to_s) }
|
data/lib/i18n_feedback/widget.js
CHANGED
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
|
|
222
222
|
function loadPrior(key) {
|
|
223
223
|
var params = new URLSearchParams({ key: key, locale: config.locale });
|
|
224
|
-
fetch(config.endpoint + "?" + params.toString(), { headers: { Accept: "application/json" } })
|
|
224
|
+
fetch(config.endpoint + "/context?" + params.toString(), { headers: { Accept: "application/json" } })
|
|
225
225
|
.then(function (response) {
|
|
226
226
|
return response.ok ? response.json() : [];
|
|
227
227
|
})
|
data/lib/i18n_feedback.rb
CHANGED
|
@@ -26,5 +26,12 @@ module I18nFeedback
|
|
|
26
26
|
def available?(request)
|
|
27
27
|
config.environment_enabled? && !!config.enabled.call(request)
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# May this request browse and triage the dashboard? Independent of
|
|
31
|
+
# `available?` — the dashboard has its own gate so maintainers can review
|
|
32
|
+
# suggestions from production even where the widget itself is off.
|
|
33
|
+
def admin?(request)
|
|
34
|
+
!!config.authorize_admin.call(request)
|
|
35
|
+
end
|
|
29
36
|
end
|
|
30
37
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n_feedback
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -43,6 +43,8 @@ files:
|
|
|
43
43
|
- app/helpers/i18n_feedback/tag_helper.rb
|
|
44
44
|
- app/models/i18n_feedback/application_record.rb
|
|
45
45
|
- app/models/i18n_feedback/suggestion.rb
|
|
46
|
+
- app/views/i18n_feedback/suggestions/index.html.erb
|
|
47
|
+
- app/views/layouts/i18n_feedback/application.html.erb
|
|
46
48
|
- config/locales/i18n_feedback.ar.yml
|
|
47
49
|
- config/locales/i18n_feedback.bg.yml
|
|
48
50
|
- config/locales/i18n_feedback.bn.yml
|