i18n_feedback 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d40ced592fbef71b06b90a276fdf459549df06666ee3dcfe2b2d91cf3fd3e0a
4
- data.tar.gz: 19ad82f30538a1611c260d132493423d36d6bba763c67e94392b3ff281612772
3
+ metadata.gz: 675e56ff8164412361ecd15886a56ef5eb7a97fdda9a766246b0d690c602d2e1
4
+ data.tar.gz: 36c511550f1b042de8052e995afeab43c27645410d0dea5246a4ffde9e9d598f
5
5
  SHA512:
6
- metadata.gz: 9800f7f1019390b0452a2605d6d66be2099346cadc2e5b89674353934214457e99b3e67b2c53cb2e17ca124b824a65bbd48d470d7ba9da5047a27f3b7f5f1e08
7
- data.tar.gz: 14e4e70d37756b56d425313d16c1b582da719257002d4a65384a7021b6d7a6670c965822c59cf0994b1fbfe5f3fdbd9d40578fdc952f6623ff3f37c65f644b9a
6
+ metadata.gz: fc37ff253e157cc2c2b619e8007bdbe698a1669a96c19c5243b5e326db8185344ee1fa49bdaf40f355240fb6322d90d84bc2e7944bef84b19c77bc381bdcf859
7
+ data.tar.gz: f9136776bb4abeb8bb1a6271e8f924178486fad4ab3923eed3e9826110daa56501ca8773962ffc8caee9b340c9b294c6747b500c38b989ed6e412ada137d6237
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.0]
6
+
7
+ - Add a built-in **triage dashboard**, mounted at the engine root (your
8
+ `mount_path`, default `/i18n_feedback`): pending / applied / rejected tabs
9
+ with counts, a per-locale filter, each suggestion shown current-vs-proposed,
10
+ and one-click Apply / Reject / Reopen / Delete. Plain server-rendered HTML
11
+ with its own self-contained styling (light + dark via `prefers-color-scheme`)
12
+ — no host assets or JS framework required.
13
+ - Every dashboard string renders through Rails I18n under `i18n_feedback.dashboard.*`
14
+ and `i18n_feedback.statuses.*`, with English fallbacks, so a host can translate
15
+ or reword any of it from its own locale files.
16
+ - Add `config.authorize_admin` — the dashboard's gate, **defaulting to
17
+ development only** so a fresh install never exposes it in production. It is
18
+ independent of `enabled` / `enabled_environments`, so a maintainer can triage
19
+ from production even where the widget is off.
20
+ - The widget's "already suggested" context now loads from
21
+ `GET {mount_path}/suggestions/context` (was the collection index, now the
22
+ dashboard). Internal to the gem; the bundled widget was updated in lockstep.
23
+
5
24
  ## [0.7.1]
6
25
 
7
26
  - 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,28 @@ blue accent stays the same in both.
233
237
 
234
238
  ## Reviewing suggestions
235
239
 
236
- Suggestions are ordinary records:
240
+ ### Triage dashboard
241
+
242
+ Mounted at your `mount_path` (default `/i18n_feedback`), the engine root is a
243
+ built-in triage dashboard: pending / applied / rejected tabs with counts, a
244
+ per-locale filter, each suggestion shown as current-vs-proposed, and one-click
245
+ **Apply** / **Reject** / **Reopen** / **Delete**. It's plain server-rendered
246
+ HTML with its own styling — no host assets or JS framework needed.
247
+
248
+ It has its own gate, `config.authorize_admin`, **defaulting to development
249
+ only** — so a fresh install never exposes it in production. Point it at your own
250
+ admin check to open it elsewhere:
251
+
252
+ ```ruby
253
+ config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
254
+ ```
255
+
256
+ The gate is independent of `enabled` / `enabled_environments`: the widget can be
257
+ dev/staging-only while a maintainer still triages from production.
258
+
259
+ ### From the console
260
+
261
+ Suggestions are also ordinary records:
237
262
 
238
263
  ```ruby
239
264
  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
- # Server-side gate. The client can set the cookie, but it can never reach the
12
- # endpoints unless the app itself says the tool is available for this request.
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,15 @@
2
2
 
3
3
  module I18nFeedback
4
4
  class SuggestionsController < ApplicationController
5
+ PER_PAGE = 50
6
+
7
+ # Public widget API is available-gated; the triage dashboard is admin-gated.
8
+ before_action :require_available, only: %i[context create]
9
+ before_action :require_admin, only: %i[index update destroy]
10
+ before_action :set_suggestion, only: %i[update destroy]
11
+
12
+ layout 'i18n_feedback/application', only: :index
13
+
5
14
  # Throttle the public submission endpoint per IP so one user or bot can't
6
15
  # flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
7
16
  # Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
@@ -15,9 +24,40 @@ module I18nFeedback
15
24
  })
16
25
  end
17
26
 
27
+ # --- triage dashboard (admin) --------------------------------------------
28
+
29
+ def index
30
+ @status = Suggestion::STATUSES.include?(params[:status]) ? params[:status] : 'pending'
31
+ @locale = params[:locale].presence
32
+ @counts = Suggestion.group(:status).count
33
+ @locales = Suggestion.distinct.pluck(:locale).compact.sort
34
+
35
+ scope = Suggestion.where(status: @status)
36
+ scope = scope.where(locale: @locale) if @locale
37
+ @page = [params[:page].to_i, 1].max
38
+ rows = scope.newest_first.offset((@page - 1) * PER_PAGE).limit(PER_PAGE + 1).to_a
39
+ @more = rows.size > PER_PAGE
40
+ @suggestions = rows.first(PER_PAGE)
41
+ end
42
+
43
+ def update
44
+ # status arrives from a dashboard button; ignore anything not a real state
45
+ # (an AR enum would otherwise raise on an unknown value).
46
+ status = params[:status].to_s
47
+ @suggestion.update!(status: status) if Suggestion::STATUSES.include?(status)
48
+ redirect_back fallback_location: root_path, status: :see_other
49
+ end
50
+
51
+ def destroy
52
+ @suggestion.destroy!
53
+ redirect_back fallback_location: root_path, status: :see_other
54
+ end
55
+
56
+ # --- widget API (public) -------------------------------------------------
57
+
18
58
  # Pending suggestions for one key/locale, shown as read-only context when the
19
59
  # proofreader reopens the popover for a string someone already flagged.
20
- def index
60
+ def context
21
61
  suggestions = Suggestion
22
62
  .where(translation_key: params[:key], locale: params[:locale])
23
63
  .status_pending
@@ -41,6 +81,10 @@ module I18nFeedback
41
81
 
42
82
  private
43
83
 
84
+ def set_suggestion
85
+ @suggestion = Suggestion.find(params[:id])
86
+ end
87
+
44
88
  def attribute_author(suggestion)
45
89
  author = current_author
46
90
  return unless author
@@ -0,0 +1,93 @@
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
+
57
+ <div class="actions">
58
+ <% unless suggestion.status_applied? %>
59
+ <%= button_to t('i18n_feedback.dashboard.apply', default: 'Apply'),
60
+ suggestion_path(suggestion, status: 'applied'), method: :patch, class: 'primary' %>
61
+ <% end %>
62
+ <% unless suggestion.status_rejected? %>
63
+ <%= button_to t('i18n_feedback.dashboard.reject', default: 'Reject'),
64
+ suggestion_path(suggestion, status: 'rejected'), method: :patch %>
65
+ <% end %>
66
+ <% unless suggestion.status_pending? %>
67
+ <%= button_to t('i18n_feedback.dashboard.reopen', default: 'Reopen'),
68
+ suggestion_path(suggestion, status: 'pending'), method: :patch %>
69
+ <% end %>
70
+ <%= button_to t('i18n_feedback.dashboard.delete', default: 'Delete'),
71
+ suggestion_path(suggestion), method: :delete, class: 'danger',
72
+ form: { data: { turbo_confirm: t('i18n_feedback.dashboard.delete_confirm', default: 'Delete this suggestion?') } } %>
73
+ </div>
74
+ </article>
75
+ <% end %>
76
+
77
+ <nav class="pager">
78
+ <span>
79
+ <% if @page > 1 %>
80
+ <%= link_to t('i18n_feedback.dashboard.newer', default: '← Newer'),
81
+ suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
82
+ <% end %>
83
+ </span>
84
+ <span>
85
+ <% if @more %>
86
+ <%= link_to t('i18n_feedback.dashboard.older', default: 'Older →'),
87
+ suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
88
+ <% end %>
89
+ </span>
90
+ </nav>
91
+ <% else %>
92
+ <div class="empty"><%= t('i18n_feedback.dashboard.empty', default: 'Nothing here yet.') %></div>
93
+ <% end %>
@@ -0,0 +1,99 @@
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
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
79
+ .actions form { display: inline; margin: 0; }
80
+ button {
81
+ padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
82
+ background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
83
+ }
84
+ button:hover { border-color: var(--muted); }
85
+ button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
86
+ button.danger { color: var(--rejected); border-color: transparent; }
87
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
88
+
89
+ .empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
90
+ .pager { margin-top: 16px; display: flex; justify-content: space-between; }
91
+ .pager .muted { color: var(--muted); }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="container">
96
+ <%= yield %>
97
+ </div>
98
+ </body>
99
+ </html>
@@ -14,3 +14,26 @@ 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
+ apply: "Apply"
33
+ reject: "Reject"
34
+ reopen: "Reopen"
35
+ delete: "Delete"
36
+ delete_confirm: "Delete this suggestion?"
37
+ empty: "Nothing here yet."
38
+ newer: "← Newer"
39
+ older: "Older →"
data/config/routes.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  I18nFeedback::Engine.routes.draw do
4
- resources :suggestions, only: %i[index create]
4
+ # index/update/destroy are the admin triage dashboard; create + the `context`
5
+ # collection route are the widget's public API (see SuggestionsController).
6
+ resources :suggestions, only: %i[index create update destroy] do
7
+ get :context, on: :collection
8
+ end
9
+
10
+ root to: 'suggestions#index'
5
11
  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) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nFeedback
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -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.7.1
4
+ version: 0.8.0
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