i18n_feedback 0.8.2 → 0.8.3

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -319
  3. data/lib/i18n_feedback.rb +12 -35
  4. metadata +14 -62
  5. data/CHANGELOG.md +0 -191
  6. data/MIT-LICENSE +0 -21
  7. data/Rakefile +0 -14
  8. data/app/controllers/i18n_feedback/application_controller.rb +0 -32
  9. data/app/controllers/i18n_feedback/suggestions_controller.rb +0 -92
  10. data/app/helpers/i18n_feedback/tag_helper.rb +0 -19
  11. data/app/models/i18n_feedback/application_record.rb +0 -7
  12. data/app/models/i18n_feedback/suggestion.rb +0 -27
  13. data/app/views/i18n_feedback/suggestions/index.html.erb +0 -77
  14. data/app/views/layouts/i18n_feedback/application.html.erb +0 -96
  15. data/config/locales/i18n_feedback.ar.yml +0 -16
  16. data/config/locales/i18n_feedback.bg.yml +0 -16
  17. data/config/locales/i18n_feedback.bn.yml +0 -16
  18. data/config/locales/i18n_feedback.de.yml +0 -16
  19. data/config/locales/i18n_feedback.el.yml +0 -16
  20. data/config/locales/i18n_feedback.en.yml +0 -35
  21. data/config/locales/i18n_feedback.es.yml +0 -16
  22. data/config/locales/i18n_feedback.fr.yml +0 -16
  23. data/config/locales/i18n_feedback.hi.yml +0 -16
  24. data/config/locales/i18n_feedback.hr.yml +0 -16
  25. data/config/locales/i18n_feedback.id.yml +0 -16
  26. data/config/locales/i18n_feedback.it.yml +0 -16
  27. data/config/locales/i18n_feedback.ja.yml +0 -16
  28. data/config/locales/i18n_feedback.ko.yml +0 -16
  29. data/config/locales/i18n_feedback.lb.yml +0 -16
  30. data/config/locales/i18n_feedback.nl.yml +0 -16
  31. data/config/locales/i18n_feedback.pl.yml +0 -16
  32. data/config/locales/i18n_feedback.pt.yml +0 -16
  33. data/config/locales/i18n_feedback.ro.yml +0 -16
  34. data/config/locales/i18n_feedback.ru.yml +0 -16
  35. data/config/locales/i18n_feedback.th.yml +0 -16
  36. data/config/locales/i18n_feedback.tr.yml +0 -16
  37. data/config/locales/i18n_feedback.uk.yml +0 -16
  38. data/config/locales/i18n_feedback.ur.yml +0 -16
  39. data/config/locales/i18n_feedback.vi.yml +0 -16
  40. data/config/locales/i18n_feedback.zh-CN.yml +0 -16
  41. data/config/routes.rb +0 -13
  42. data/lib/generators/i18n_feedback/install/install_generator.rb +0 -40
  43. data/lib/generators/i18n_feedback/install/templates/create_i18n_feedback_suggestions.rb.tt +0 -22
  44. data/lib/generators/i18n_feedback/install/templates/initializer.rb +0 -56
  45. data/lib/i18n_feedback/configuration.rb +0 -91
  46. data/lib/i18n_feedback/engine.rb +0 -26
  47. data/lib/i18n_feedback/marking.rb +0 -46
  48. data/lib/i18n_feedback/middleware.rb +0 -145
  49. data/lib/i18n_feedback/version.rb +0 -5
  50. data/lib/i18n_feedback/widget.js +0 -443
  51. data/lib/i18n_feedback/widget.rb +0 -96
data/CHANGELOG.md DELETED
@@ -1,191 +0,0 @@
1
- # Changelog
2
-
3
- ## [Unreleased]
4
-
5
- ## [0.8.2]
6
-
7
- - Fix the dashboard's locale filter doing nothing under a strict (nonce-based)
8
- Content-Security-Policy: it relied on an inline `onchange` handler, which such
9
- a CSP blocks. The filter now has an always-visible **Filter** submit button, so
10
- it works with JS off or inline handlers blocked (the `onchange` stays as a
11
- nicety where allowed).
12
- - Give the locale filter a visible **Locale** label.
13
-
14
- ## [0.8.1]
15
-
16
- - Make the dashboard **read-only**. Apply / Reject / Reopen / Delete only ever
17
- flipped the stored `status` — they never touched the host's locale files, so
18
- offering them implied a capability the gem doesn't have. The buttons and their
19
- `update` / `destroy` routes and actions are removed; the dashboard now just
20
- lists suggestions for review, and you make the edits in your own locale files.
21
- A suggestion's `status` still exists on the model for out-of-band tracking and
22
- as groundwork for a future "apply to locale file" feature.
23
-
24
- ## [0.8.0]
25
-
26
- - Add a built-in **triage dashboard**, mounted at the engine root (your
27
- `mount_path`, default `/i18n_feedback`): pending / applied / rejected tabs
28
- with counts, a per-locale filter, each suggestion shown current-vs-proposed,
29
- and one-click Apply / Reject / Reopen / Delete. Plain server-rendered HTML
30
- with its own self-contained styling (light + dark via `prefers-color-scheme`)
31
- — no host assets or JS framework required.
32
- - Every dashboard string renders through Rails I18n under `i18n_feedback.dashboard.*`
33
- and `i18n_feedback.statuses.*`, with English fallbacks, so a host can translate
34
- or reword any of it from its own locale files.
35
- - Add `config.authorize_admin` — the dashboard's gate, **defaulting to
36
- development only** so a fresh install never exposes it in production. It is
37
- independent of `enabled` / `enabled_environments`, so a maintainer can triage
38
- from production even where the widget is off.
39
- - The widget's "already suggested" context now loads from
40
- `GET {mount_path}/suggestions/context` (was the collection index, now the
41
- dashboard). Internal to the gem; the bundled widget was updated in lockstep.
42
-
43
- ## [0.7.1]
44
-
45
- - The `i18n_feedback.stop` toggle label now reads "Stop suggesting (Esc)" in
46
- every language — Esc already exits suggest mode, so the control says so.
47
- - The widget JavaScript moved from `app/assets/` to `lib/` (it was always
48
- inlined server-side, never served as an asset). Rails auto-registers every
49
- engine's `app/assets/*` directory with the host's asset pipeline, so
50
- Propshaft hosts were ingesting the file into their asset namespace under
51
- the bare logical name `widget.js` — colliding with any other gem or host
52
- file of the same name — and needlessly digesting a public copy at
53
- precompile. The gem is now invisible to Sprockets/Propshaft entirely.
54
- No behavior change.
55
-
56
- ## [0.7.0]
57
-
58
- - Reword the `i18n_feedback.start` toggle label from "Suggest edits" to
59
- "Improve translation" (localized in every language) — clearer for a menu/link
60
- entry. The floating pill's own label (`pill`) is unchanged.
61
- - Ship 5 more languages: Bulgarian, Greek, Croatian, Luxembourgish, and
62
- Romanian — 25 languages besides English now bundled.
63
-
64
- ## [0.6.1]
65
-
66
- - Shorten the active-pill label from "Suggesting — tap to exit (Esc)" to
67
- "Stop suggesting (Esc)" across all bundled languages.
68
- - Release workflow: skip the flaky post-publish propagation probe
69
- (`await-release: false`) so a successful push no longer fails the job.
70
-
71
- ## [0.6.0]
72
-
73
- - Ship `i18n_feedback.start` / `i18n_feedback.stop` labels in every bundled
74
- language, for hosts that drive suggest mode from their own menu/link instead
75
- of the floating pill. Because the `i18n_feedback.*` scope is exempt from
76
- key-marking, these are safe to render with `t(...)` — no plain-literal
77
- workaround needed. See the "Toggling suggest mode from your own link" README
78
- section for a full HTML example.
79
-
80
- ## [0.5.0]
81
-
82
- - Add a `status` to each suggestion — a string-backed Active Record enum with
83
- values `pending`, `applied`, and `rejected` (`I18nFeedback::Suggestion::STATUSES`),
84
- `status_`-prefixed (`status_applied?`, `status_applied!`, `Suggestion.status_pending`)
85
- plus a `newest_first` scope. New suggestions start `pending`; the popover's
86
- "already suggested" context now shows only pending ones, so applied/rejected
87
- wordings stop resurfacing.
88
-
89
- **Upgrading an existing install:** add the column with a migration —
90
-
91
- ```ruby
92
- add_column :i18n_feedback_suggestions, :status, :string, null: false, default: "pending"
93
- add_index :i18n_feedback_suggestions, :status
94
- ```
95
-
96
- - Harden the public submission endpoint against abuse:
97
- - Per-IP rate limiting via Rails' built-in limiter (Rails 7.2+; a no-op on
98
- 7.1). Default 30 requests / 60s, tunable or disable-able via
99
- `config.rate_limit`; returns `429 Too Many Requests`.
100
- - Length caps on the stored free-text fields (`proposed_value` / `old_value`
101
- 5000, `comment` / `page_url` 2000, `translation_key` 500) so a client can't
102
- bloat the table with unbounded input.
103
-
104
- - Add a `.github/workflows/release.yml` that publishes to RubyGems via trusted
105
- publishing (OIDC) when a `v*` tag is pushed — no stored credentials or MFA
106
- prompt.
107
-
108
- ## [0.4.0]
109
-
110
- - Add a `config.on_submit` hook, called with each saved suggestion right after
111
- it's stored — notify Slack, send an email, open a ticket. Runs inline after
112
- save, so keep it fast or hand off to a job.
113
-
114
- ## [0.3.1]
115
-
116
- - Fix the widget showing raw key markers (e.g. `⟦i18n_feedback.title⟧`) in its own
117
- popover while suggest mode was on. The key-marking backend was tagging the
118
- tool's own `i18n_feedback.*` strings; those are not part of the host app's
119
- translatable copy, so they're now always skipped — the widget never marks or
120
- offers to edit its own UI.
121
- - Fix the popover not following the page's language under auto-injection. The
122
- widget is injected in middleware *after* the controller action, so an
123
- `around_action { I18n.with_locale(...) }` had already reset `I18n.locale` back
124
- to the default — the popover (and the saved suggestion's `locale`) came out in
125
- the wrong language. The locale is now read from the page's rendered
126
- `<html lang>` attribute and labels resolve under it explicitly.
127
-
128
- ## [0.3.0]
129
-
130
- - Localize the widget's own UI. Every string in the pill and the suggestion
131
- popover now resolves through Rails I18n under the `i18n_feedback.*` scope and
132
- follows the app's current `I18n.locale`, so the tool speaks the same language as
133
- the app being proofread. Any key a host hasn't translated falls back to English
134
- — so nothing goes blank in a locale you haven't fully covered. Override any
135
- string by defining the matching key in your own locale files.
136
- - Ship translations out of the box for 20 languages in addition to English:
137
- Arabic, Bengali, Chinese (Simplified), Dutch, French, German, Hindi, Indonesian,
138
- Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish, Thai, Turkish,
139
- Ukrainian, Urdu and Vietnamese.
140
- - Render the popover right-to-left for RTL locales (Arabic, Urdu, and other RTL
141
- scripts), detected from the active locale's language subtag. The i18n key stays
142
- left-to-right, since it's a code identifier rather than prose.
143
- - `config.pill_label` now defaults to `nil`, meaning "use the localized default".
144
- Setting it to a string still overrides the pill text as before.
145
- - The widget now follows the operating system's light/dark/system appearance via
146
- `prefers-color-scheme`. The pill and popover render with a dark surface when the
147
- reviewer's system is in dark mode, with no configuration required.
148
-
149
- ## [0.2.2]
150
-
151
- - Fix suggest mode desyncing under a nonce-based CSP on Turbo visits. The runtime
152
- config now rides in a `<script type="application/json">` block (data, not code)
153
- that the widget re-reads on every `turbo:load`, instead of an executable
154
- `<script>` the browser refuses to re-run when Turbo re-evaluates it with a stale
155
- nonce. Only the widget code carries the CSP nonce now.
156
- - Treat `?i18n_feedback=true|false` as a one-shot command: the middleware sets the
157
- cookie and redirects (303) to the same URL without the parameter. The cookie is
158
- now the single source of truth, so the parameter no longer sticks in the address
159
- bar and the pill's reload can turn suggest mode off.
160
- - Let a host's own toggle link work while suggest mode is active. Suggest mode
161
- freezes navigation so a stray click can't leave the page mid-proofread, but that
162
- also froze a `?i18n_feedback=false` link in your own nav — so the only way out
163
- was the pill. Links carrying the toggle parameter are now exempt from the freeze.
164
-
165
- ## [0.2.1]
166
-
167
- - Keep the suggest pill and active-mode highlighting working across Turbo Drive
168
- navigations. Turbo replaces `<body>` on each visit, which removed the pill; the
169
- widget now re-runs its per-page setup on `turbo:load` instead of only on the
170
- initial load, so it no longer requires a hard reload.
171
-
172
- ## [0.2.0]
173
-
174
- - Stamp the injected widget scripts with the request's Content-Security-Policy
175
- nonce when one is present, so the tool works under a nonce-based CSP (including
176
- `strict-dynamic`). No-op for apps without a CSP nonce.
177
-
178
- ## [0.1.0]
179
-
180
- - Initial release.
181
- - In-context i18n key markers, gated to configured environments and toggled per
182
- request by the widget.
183
- - Self-contained browser widget (no CSS or JS framework required) with a suggest
184
- pill and a per-string suggestion popover. The copy cursor and hover outline
185
- appear only on the strings that resolve to a key.
186
- - Optional floating pill (`config.show_pill`) and a URL toggle
187
- (`?i18n_feedback=true` / `false`, remembered in a cookie) so suggest mode can
188
- be triggered from a host-provided link instead.
189
- - `I18nFeedback::Suggestion` model and mountable engine endpoints for listing and
190
- creating suggestions.
191
- - `i18n_feedback:install` generator (initializer, migration, engine mount).
data/MIT-LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Yaroslav Shmarov
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
data/Rakefile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- # Defines build/install/release — `rake release` is what the trusted-publishing
5
- # workflow (.github/workflows/release.yml) runs to push the gem to RubyGems.
6
- require 'bundler/gem_tasks'
7
-
8
- APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
9
- load 'rails/tasks/engine.rake' if File.exist?(APP_RAKEFILE)
10
-
11
- require 'rspec/core/rake_task'
12
- RSpec::Core::RakeTask.new(:spec)
13
-
14
- task default: :spec
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18nFeedback
4
- class ApplicationController < ActionController::Base
5
- protect_from_forgery with: :exception
6
-
7
- private
8
-
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.
12
- def require_available
13
- head :forbidden unless I18nFeedback.available?(request)
14
- end
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
-
26
- def current_author
27
- return @current_author if defined?(@current_author)
28
-
29
- @current_author = I18nFeedback.config.current_user.call(request)
30
- end
31
- end
32
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18nFeedback
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
-
13
- # Throttle the public submission endpoint per IP so one user or bot can't
14
- # flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
15
- # Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
16
- # read once at boot, after the host's initializer.
17
- if respond_to?(:rate_limit) && I18nFeedback.config.rate_limit
18
- rate_limit(**I18nFeedback.config.rate_limit,
19
- only: :create,
20
- with: lambda {
21
- render json: { errors: ['Too many suggestions. Please slow down and try again.'] },
22
- status: :too_many_requests
23
- })
24
- end
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
-
44
- # Pending suggestions for one key/locale, shown as read-only context when the
45
- # proofreader reopens the popover for a string someone already flagged.
46
- def context
47
- suggestions = Suggestion
48
- .where(translation_key: params[:key], locale: params[:locale])
49
- .status_pending
50
- .newest_first
51
- .limit(20)
52
-
53
- render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
54
- end
55
-
56
- def create
57
- suggestion = Suggestion.new(suggestion_params)
58
- attribute_author(suggestion)
59
-
60
- if suggestion.save
61
- I18nFeedback.config.on_submit.call(suggestion)
62
- head :created
63
- else
64
- render json: { errors: suggestion.errors.full_messages }, status: :unprocessable_entity
65
- end
66
- end
67
-
68
- private
69
-
70
- def attribute_author(suggestion)
71
- author = current_author
72
- return unless author
73
-
74
- suggestion.author_id = author.id.to_s if author.respond_to?(:id)
75
- suggestion.author_label = I18nFeedback.config.author_label.call(author)
76
- end
77
-
78
- def suggestion_json(suggestion)
79
- {
80
- proposed_value: suggestion.proposed_value,
81
- author_label: suggestion.author_label,
82
- created_at: suggestion.created_at.iso8601
83
- }
84
- end
85
-
86
- def suggestion_params
87
- params
88
- .require(:suggestion)
89
- .permit(:translation_key, :locale, :old_value, :proposed_value, :comment, :page_url)
90
- end
91
- end
92
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18nFeedback
4
- # Lets a host place the widget explicitly (`<%= i18n_feedback_tag %>` at the end
5
- # of a layout) instead of relying on auto-injection. Renders nothing unless the
6
- # tool is available for the current request.
7
- module TagHelper
8
- def i18n_feedback_tag
9
- return ''.html_safe unless I18nFeedback.available?(request)
10
-
11
- Widget.snippet(
12
- endpoint: I18nFeedback.config.suggestions_endpoint,
13
- locale: I18n.locale,
14
- active: I18nFeedback::Marking.enabled?,
15
- nonce: (content_security_policy_nonce if respond_to?(:content_security_policy_nonce))
16
- ).html_safe
17
- end
18
- end
19
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18nFeedback
4
- class ApplicationRecord < ActiveRecord::Base
5
- self.abstract_class = true
6
- end
7
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module I18nFeedback
4
- # A proposed wording for one translation key in one locale. Author attribution
5
- # is optional and stored as loose fields (no foreign key to the host's user
6
- # table) so the model is portable across apps with different user models.
7
- class Suggestion < ApplicationRecord
8
- # Lifecycle of a proposed wording: a fresh suggestion is `pending` until a
9
- # developer applies it to the locale files or rejects it. String-backed so
10
- # the column stays human-readable; prefixed so the generated methods read as
11
- # `status_pending?` / `Suggestion.status_applied` and never clash.
12
- STATUSES = %w[pending applied rejected].freeze
13
-
14
- enum :status, STATUSES.index_by(&:itself), prefix: :status, default: :pending
15
-
16
- validates :translation_key, presence: true, length: { maximum: 500 }
17
- validates :proposed_value, presence: true, length: { maximum: 5_000 }
18
- validates :old_value, length: { maximum: 5_000 }
19
- validates :comment, length: { maximum: 2_000 }
20
- validates :page_url, length: { maximum: 2_000 }
21
- validates :locale,
22
- presence: true,
23
- inclusion: { in: ->(_) { I18nFeedback.config.available_locales.call.map(&:to_s) } }
24
-
25
- scope :newest_first, -> { order(id: :desc) }
26
- end
27
- end
@@ -1,77 +0,0 @@
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
- <label for="i18nf-locale"><%= t('i18n_feedback.dashboard.locale', default: 'Locale') %></label>
20
- <select id="i18nf-locale" name="locale" onchange="this.form.submit()">
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
- <%# Inline onchange auto-submits where allowed; this button is the reliable
27
- path where a strict CSP blocks inline handlers, or with JS off. %>
28
- <button><%= t('i18n_feedback.dashboard.filter', default: 'Filter') %></button>
29
- </form>
30
- <% end %>
31
-
32
- <% if @suggestions.any? %>
33
- <% @suggestions.each do |suggestion| %>
34
- <article class="card">
35
- <div class="row-top">
36
- <code class="key"><%= suggestion.translation_key %></code>
37
- <span class="badge locale"><%= suggestion.locale %></span>
38
- <span class="badge status-<%= suggestion.status %>">
39
- <%= t("i18n_feedback.statuses.#{suggestion.status}", default: suggestion.status.humanize) %>
40
- </span>
41
- <span class="spacer"></span>
42
- <span class="meta">
43
- <% if suggestion.author_label.present? %><%= suggestion.author_label %> · <% end %>
44
- <span title="<%= suggestion.created_at %>"><%= time_ago_in_words(suggestion.created_at) %> <%= t('i18n_feedback.dashboard.ago', default: 'ago') %></span>
45
- </span>
46
- </div>
47
-
48
- <dl class="diff">
49
- <dt><%= t('i18n_feedback.dashboard.current', default: 'Current') %></dt>
50
- <dd><span class="old" dir="auto"><%= suggestion.old_value.presence || '—' %></span></dd>
51
- <dt><%= t('i18n_feedback.dashboard.suggested', default: 'Suggested') %></dt>
52
- <dd><span class="new" dir="auto"><%= suggestion.proposed_value %></span></dd>
53
- </dl>
54
-
55
- <% if suggestion.comment.present? %>
56
- <div class="comment" dir="auto"><%= suggestion.comment %></div>
57
- <% end %>
58
- </article>
59
- <% end %>
60
-
61
- <nav class="pager">
62
- <span>
63
- <% if @page > 1 %>
64
- <%= link_to t('i18n_feedback.dashboard.newer', default: '← Newer'),
65
- suggestions_path(status: @status, locale: @locale, page: @page - 1) %>
66
- <% end %>
67
- </span>
68
- <span>
69
- <% if @more %>
70
- <%= link_to t('i18n_feedback.dashboard.older', default: 'Older →'),
71
- suggestions_path(status: @status, locale: @locale, page: @page + 1) %>
72
- <% end %>
73
- </span>
74
- </nav>
75
- <% else %>
76
- <div class="empty"><%= t('i18n_feedback.dashboard.empty', default: 'Nothing here yet.') %></div>
77
- <% end %>
@@ -1,96 +0,0 @@
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 label { color: var(--muted); font-size: 13px; }
47
- .filters select {
48
- padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
49
- background: var(--surface); color: var(--text); font: inherit;
50
- }
51
-
52
- .card {
53
- background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
54
- padding: 14px 16px; margin-bottom: 12px;
55
- }
56
- .row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
57
- code.key {
58
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
59
- background: var(--code); padding: 2px 7px; border-radius: 6px; overflow-wrap: anywhere;
60
- }
61
- .badge {
62
- display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
63
- color: #fff; white-space: nowrap;
64
- }
65
- .badge.status-pending { background: var(--pending); }
66
- .badge.status-applied { background: var(--applied); }
67
- .badge.status-rejected { background: var(--rejected); }
68
- .badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
69
- .spacer { flex: 1; }
70
- .meta { color: var(--muted); font-size: 13px; }
71
-
72
- dl.diff { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
73
- dl.diff dt { color: var(--muted); font-size: 13px; }
74
- dl.diff dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
75
- dd .old { color: var(--muted); }
76
- dd .new { font-weight: 600; }
77
- .comment { margin-top: 10px; padding: 8px 10px; background: var(--code); border-radius: 8px; font-size: 14px; overflow-wrap: anywhere; }
78
-
79
- button {
80
- padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
81
- background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
82
- }
83
- button:hover { border-color: var(--muted); }
84
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
85
-
86
- .empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
87
- .pager { margin-top: 16px; display: flex; justify-content: space-between; }
88
- .pager .muted { color: var(--muted); }
89
- </style>
90
- </head>
91
- <body>
92
- <div class="container">
93
- <%= yield %>
94
- </div>
95
- </body>
96
- </html>
@@ -1,16 +0,0 @@
1
- ar:
2
- i18n_feedback:
3
- pill: "اقترح تعديلات"
4
- start: "تحسين الترجمة"
5
- stop: "إيقاف الاقتراح (Esc)"
6
- pill_active: "إيقاف الاقتراح (Esc)"
7
- title: "اقترح تصحيحًا للترجمة"
8
- current_text: "النص الحالي"
9
- suggested_text: "النص المقترح"
10
- comment: "تعليق"
11
- comment_placeholder: "ملاحظة اختيارية للمطور"
12
- prior_title: "تم اقتراحه مسبقًا (قيد الانتظار)"
13
- cancel: "إلغاء"
14
- save: "إرسال الاقتراح"
15
- error_blank: "يرجى إدخال اقتراح."
16
- error_save: "تعذّر حفظ الاقتراح."
@@ -1,16 +0,0 @@
1
- bg:
2
- i18n_feedback:
3
- pill: "Предложи промени"
4
- pill_active: "Спиране на предложенията (Esc)"
5
- start: "Подобряване на превода"
6
- stop: "Спиране на предложенията (Esc)"
7
- title: "Предложете корекция на превода"
8
- current_text: "Текущ текст"
9
- suggested_text: "Предложен текст"
10
- comment: "Коментар"
11
- comment_placeholder: "Незадължителна бележка за разработчика"
12
- prior_title: "Вече предложено (чакащо)"
13
- cancel: "Отказ"
14
- save: "Изпрати предложение"
15
- error_blank: "Моля, въведете предложение."
16
- error_save: "Предложението не можа да бъде запазено."
@@ -1,16 +0,0 @@
1
- bn:
2
- i18n_feedback:
3
- pill: "সম্পাদনা প্রস্তাব করুন"
4
- start: "অনুবাদ উন্নত করুন"
5
- stop: "প্রস্তাব দেওয়া বন্ধ করুন (Esc)"
6
- pill_active: "প্রস্তাব দেওয়া বন্ধ করুন (Esc)"
7
- title: "অনুবাদ সংশোধন প্রস্তাব করুন"
8
- current_text: "বর্তমান লেখা"
9
- suggested_text: "প্রস্তাবিত লেখা"
10
- comment: "মন্তব্য"
11
- comment_placeholder: "ডেভেলপারের জন্য ঐচ্ছিক নোট"
12
- prior_title: "ইতিমধ্যে প্রস্তাবিত (অপেক্ষমাণ)"
13
- cancel: "বাতিল"
14
- save: "প্রস্তাব পাঠান"
15
- error_blank: "অনুগ্রহ করে একটি প্রস্তাব লিখুন।"
16
- error_save: "প্রস্তাব সংরক্ষণ করা যায়নি।"
@@ -1,16 +0,0 @@
1
- de:
2
- i18n_feedback:
3
- pill: "Änderungen vorschlagen"
4
- start: "Übersetzung verbessern"
5
- stop: "Vorschläge beenden (Esc)"
6
- pill_active: "Vorschläge beenden (Esc)"
7
- title: "Übersetzungskorrektur vorschlagen"
8
- current_text: "Aktueller Text"
9
- suggested_text: "Vorgeschlagener Text"
10
- comment: "Kommentar"
11
- comment_placeholder: "Optionale Notiz für die Entwicklung"
12
- prior_title: "Bereits vorgeschlagen (ausstehend)"
13
- cancel: "Abbrechen"
14
- save: "Vorschlag senden"
15
- error_blank: "Bitte gib einen Vorschlag ein."
16
- error_save: "Der Vorschlag konnte nicht gespeichert werden."