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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92f93f3b80d87b86e0ec3b0ce33dc7c89cdd5ba9c2e7fc8b40bcb26b92a1a224
4
- data.tar.gz: f994d6b60277c364a23eb299e344ab0af2f1fcdaa567966d07bd97352ac7f2fe
3
+ metadata.gz: 3b4d3167c04697942a2b695b1f4b81554a4f12daabc6a8d730b0d4374bb31292
4
+ data.tar.gz: 95f184235ea7319ebdd254592924c25f8503ac0fee7943282cd939478d037a54
5
5
  SHA512:
6
- metadata.gz: d99a83e338ef612aa9a519eb115cb55f55a35978c514171070848e4dca9ea470dbb9055d29f04e5b950107611181a8719d8eff9d976272d7f4c67f8694186ff3
7
- data.tar.gz: 3a296e3305dcce5f55b22d01843f5a885a34d805f30f4dcfa794a08138948f06125a438d550c03e142409d1a3e60e6ebf32259dd470f930b42c945b0e892340b
6
+ metadata.gz: 564ff0282d7ba45a0ef399c82ff6b287dfe54108512aa91356769a6e652fa66875bb3a04c195aed57bce3507eabf50a0d7505ec386ebbe34ed0186e7c8ff065f
7
+ data.tar.gz: de5b07512272838e8f15483978cb536d0f4ae55e33fe6c5f17d1d3d7b4b5430badf6bfc54c96a01f37802976565afc287c78f5ccc3d40924e41f7e54137fae7e
data/AGENTS.md ADDED
@@ -0,0 +1,149 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for coding agents. Two audiences:
4
+
5
+ - **[Installing i18n_proofreading into a Rails app](#installing-into-a-rails-app)** — you are working in a host app and were asked to add in-context translation review or a way for someone to suggest better wording.
6
+ - **[Working on the gem itself](#working-on-the-gem-itself)** — you are working in this repository.
7
+
8
+ Requirements: Ruby >= 3.2, Rails >= 7.1. The widget needs the CSRF token from `csrf_meta_tags`, which a standard Rails layout already has.
9
+
10
+ **Read this first: the gem never writes to your locale files, and it is not a production tool.** Both are deliberate, and both are covered below.
11
+
12
+ If you are in a host app and this file is not in front of you, it ships inside the gem: `cat "$(bundle show i18n_proofreading)/AGENTS.md"`.
13
+
14
+ ---
15
+
16
+ ## Installing into a Rails app
17
+
18
+ ### 1. Install
19
+
20
+ ```bash
21
+ bundle add i18n_proofreading
22
+ bin/rails generate i18n_proofreading:install
23
+ bin/rails db:migrate
24
+ ```
25
+
26
+ The generator writes `config/initializers/i18n_proofreading.rb`, one migration (`i18n_proofreading_suggestions`), and mounts the engine. Read the initializer it wrote — every option is documented there in comments, and it is the source of truth over any summary of it, including this file.
27
+
28
+ ### 2. There is no step 2 — do not edit the layout
29
+
30
+ The widget injects itself into HTML responses through a Rack middleware, so **no layout change is needed**. Boot the app in development and look for the **"Suggest edits"** pill bottom-left. Click it, then click any text; `Esc` exits.
31
+
32
+ Only if the host prefers to place it explicitly:
33
+
34
+ ```ruby
35
+ config.auto_inject = false
36
+ ```
37
+
38
+ ```erb
39
+ <%= i18n_proofreading_tag %>
40
+ ```
41
+
42
+ Do not do both. And do not go looking for a missing `<%= … %>` when the pill does not appear — auto-injection is the default, so the cause is almost always the environment gate below.
43
+
44
+ ### 3. Before deploying: understand the two gates
45
+
46
+ **a. Environments.** The tool is active only in `config.enabled_environments`, which defaults to `%w[development staging]`. In every other environment it does nothing at all: no key markers, no endpoint, and the I18n backend patch that marks keys is never even prepended. **Do not add `production` to that list to "let the client review the live site."** Marking every translated string in production is a user-visible change to every page, and the review endpoint is not built to be public.
47
+
48
+ **b. The dashboard.** `/i18n_proofreading` defaults to **development only** and is independent of the widget gates, so a maintainer can triage from production while the widget stays off. It fails closed:
49
+
50
+ ```ruby
51
+ config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
52
+ ```
53
+
54
+ > **`enabled`, `authorize_admin` and `current_user` receive the raw `request`, not a controller.** Writing `->(request) { current_user }` is the most common mistake here — that method does not exist in this scope. Resolve the user *from the request*: Warden env, a signed cookie, `Current.user` if middleware already set it. `author_label` is the exception: it receives whatever `current_user` returned.
55
+
56
+ ### 4. Verify
57
+
58
+ ```bash
59
+ bin/rails routes | grep i18n_proofreading # engine mounted
60
+ bin/rails i18n_proofreading:seed_demo # optional sample suggestions, idempotent
61
+ ```
62
+
63
+ Then in development: load a page, confirm the pill appears bottom-left, suggest a change to any string, and read it back at `/i18n_proofreading`.
64
+
65
+ ### The gem does not apply suggestions, and that is the point
66
+
67
+ A suggestion is a row: the i18n key, the old value, the proposal, an optional comment, a locale, and a status (`pending` / `applied` / `rejected`). Nothing in the gem edits `config/locales/*.yml`, and **the dashboard is read-only — `index` and `show`, with no update or destroy action.** Applying a wording change means a human (or your own tooling) editing the YAML and committing it.
68
+
69
+ So: **do not ask this gem to rewrite locale files, and do not build a "click to apply" button expecting an endpoint to exist.** If the app wants status bookkeeping, do it deliberately from your own code or the console — the enum is prefixed:
70
+
71
+ ```ruby
72
+ I18nProofreading::Suggestion.status_pending.find(id).status_applied!
73
+ ```
74
+
75
+ If you are asked to automate applying suggestions, that is host-app work: read the rows, write the YAML, review the diff in a pull request. Treat the suggestion table as an inbox, not as the source of truth for translations.
76
+
77
+ ### Turning suggest mode on from your own UI
78
+
79
+ The pill is one way; a link is another, and the choice is remembered in a cookie so the rest of the app stays in suggest mode:
80
+
81
+ ```
82
+ ?i18n_proofreading=true # on
83
+ ?i18n_proofreading=false # off
84
+ ```
85
+
86
+ `config.show_pill = false` hides the pill, `config.pill_label` overrides its text (nil = the localized `i18n_proofreading.pill` key), and `config.toggle_param` renames the parameter.
87
+
88
+ ### Do not
89
+
90
+ - **Do not add `production` to `enabled_environments`** (see above).
91
+ - **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** The middleware injects what is needed and the engine serves the code same-origin — which is what lets it run under a nonce-based CSP, including `strict-dynamic`, across Turbo body swaps.
92
+ - **Do not edit the layout** for the default install — auto-injection is the default.
93
+ - **Do not expect the gem to write YAML** (see above).
94
+ - **Do not set config outside the initializer.** `rate_limit` in particular is read once when the controller class loads; assigning config per-request mutates it process-wide.
95
+
96
+ ### Configuration worth knowing
97
+
98
+ Everything is optional; a fresh install works with zero config in development. Full list with comments is in the generated initializer.
99
+
100
+ | Option | Default | Note |
101
+ | --- | --- | --- |
102
+ | `enabled_environments` | `%w[development staging]` | The hard gate. Never add production |
103
+ | `enabled` | everyone | Per-request gate on top of the environment check |
104
+ | `authorize_admin` | development only | **Who can read the dashboard.** Independent of the above |
105
+ | `admin_layout` | `i18n_proofreading/application` | Render the dashboard in your admin shell |
106
+ | `current_user` | `nil` | Receives the request |
107
+ | `author_label` | email, else `to_s` | Receives the user |
108
+ | `available_locales` | `I18n.available_locales` | Callable; validates what a suggestion may target |
109
+ | `auto_inject` | `true` | `false` = place `i18n_proofreading_tag` yourself |
110
+ | `show_pill`, `pill_label` | `true`, localized | Hide the pill and use `?i18n_proofreading=true` instead |
111
+ | `toggle_param` | `"i18n_proofreading"` | Rename the query parameter |
112
+ | `rate_limit` | `{ to: 30, within: 60 }` | Rails 7.2+; ignored on 7.1. `nil` disables |
113
+ | `mount_path` | `"/i18n_proofreading"` | Keep in sync with the `mount` line |
114
+ | `on_submit` | no-op | Runs inline after save — Slack, email, a ticket |
115
+
116
+ The tool's own UI ships in 26 languages, RTL mirrored, and follows system light/dark.
117
+
118
+ ### Common failure modes
119
+
120
+ | Symptom | Cause |
121
+ | --- | --- |
122
+ | No pill, no outlines | The environment is not in `enabled_environments` (this is the usual one), or `config.enabled` returned false, or `show_pill = false` |
123
+ | Pill appears but no strings are outlined | The I18n backend patch is only prepended in an enabled environment at boot — check you are actually in development/staging, and restart after changing the setting |
124
+ | `/i18n_proofreading` returns a 403 | `authorize_admin` still at its development-only default |
125
+ | Suggestions rejected with an invalid-token error | The layout is missing `csrf_meta_tags` |
126
+ | A suggestion is rejected as an invalid locale | It must be in `config.available_locales` |
127
+ | Nothing changes in the app after a suggestion is accepted | Expected. The gem never writes locale files — a human edits the YAML |
128
+ | `undefined local variable current_user` in the initializer | A gate lambda treated its argument as a controller. It is a `request` |
129
+
130
+ ---
131
+
132
+ ## Working on the gem itself
133
+
134
+ ```bash
135
+ bundle exec rake test # minitest, dummy app under test/dummy
136
+ bundle exec rubocop # must be clean
137
+ BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test # 7.1, 7.2, 8.0, 8.1 in gemfiles/
138
+ ```
139
+
140
+ Layout: `app/` controller, `Suggestion`, dashboard views · `lib/i18n_proofreading/` config, middleware, the I18n marking backend, widget JS, seeds, engine · `lib/generators/i18n_proofreading/install/` the one generator · `config/locales/` 26 locales · `test/` minitest with `test/dummy` as the host app.
141
+
142
+ Conventions this codebase holds to — follow them rather than the first thing that works:
143
+
144
+ - **Production carries none of it.** The marking backend is prepended in `after_initialize` only when `environment_enabled?`, so a production boot never even patches I18n. Anything new must keep that property: no markers, no endpoint, no patch outside the enabled environments.
145
+ - **The tool never writes to the host's locale files.** That is why there is deliberately no update or destroy route, and why the dashboard is read-only. Do not add an "apply" action that edits YAML.
146
+ - **The widget is injected by middleware and served same-origin**, which is what keeps it working under a nonce-based CSP with `strict-dynamic` across Turbo body swaps. Do not inline it.
147
+ - **Key marking must degrade to plain strings.** A host that reads translations outside a request, or in an environment where the tool is off, has to get ordinary values back.
148
+ - Every user-facing change bumps `lib/i18n_proofreading/version.rb` and adds a `CHANGELOG.md` entry (Keep a Changelog format) that says what it costs, not only what it adds.
149
+ - Commit messages are prose that explains the tradeoff — read `git log` before writing one.
data/CHANGELOG.md CHANGED
@@ -1,7 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ - **`config.admin_layout` now works on its own.** The dashboard's stylesheet was
6
+ declared in the gem's layout, so replacing that layout dropped it and the
7
+ dashboard rendered unstyled. It moves into the views, so every layout gets it
8
+ with nothing asked of the host.
9
+ - **The dashboard stylesheet no longer claims selectors it does not own.** It
10
+ styled bare `*`, `body` and `a`, and its `.container`, `.card` and `.tabs` are
11
+ names other frameworks use too. Component rules now nest inside an
12
+ `.ip-dashboard` wrapper the views render, and every custom property is `--ip-`
13
+ prefixed — that collision ran both ways, so a host defining `--bg` recoloured
14
+ the dashboard just as easily.
15
+ - **Added `config.base_controller_class`.** Name the controller your own admin
16
+ inherits from and the dashboard adopts its layout, helpers, authentication and
17
+ request context. Default is unchanged.
18
+ - **The widget's endpoints moved to `I18nProofreading::SubmissionsController`.**
19
+ One controller served both them and the triage dashboard, so
20
+ `base_controller_class` would have put staff authentication in front of every
21
+ proofreader. `POST /suggestions` and `GET /suggestions/context` now route
22
+ there; both URLs are unchanged, and the per-IP rate limiter moved with the
23
+ action. If you referenced `SuggestionsController#create` or `#context`, that is
24
+ the breaking change in this release.
25
+ - **Migrations follow the host's `primary_key_type`,** the same
26
+ `Rails.configuration.generators` lookup Rails' own Active Storage migration
27
+ does, so a uuid-keyed host gets a uuid table.
28
+ - A `BackboneTest` now fails the build on any of the above regressing.
29
+
3
30
  ## [Unreleased]
4
31
 
32
+ ## [0.10.6]
33
+
34
+ - Added `AGENTS.md`: install and integration instructions written for coding
35
+ agents — that no layout change is needed because the widget is injected by
36
+ middleware, that the environment gate (never production) is the usual reason
37
+ the pill is missing, the request-shaped config lambdas, and above all that the
38
+ gem never writes to locale files, so there is no "apply" endpoint to build
39
+ against. It ships inside the gem, so
40
+ `cat "$(bundle show i18n_proofreading)/AGENTS.md"` works from a host app.
41
+ - Corrected the `authorize_admin` comment in the configuration: it described the
42
+ dashboard as changing status and deleting, but the dashboard has been
43
+ read-only (`index` and `show` only) for as long as that route file has said so.
44
+
5
45
  ## [0.10.5]
6
46
 
7
47
  - Added `config.admin_layout`, letting host apps render the proofreading
data/README.md CHANGED
@@ -26,6 +26,16 @@ bin/rails generate i18n_proofreading:install
26
26
  bin/rails db:migrate
27
27
  ```
28
28
 
29
+ Optional demo data:
30
+
31
+ ```bash
32
+ bin/rails i18n_proofreading:seed_demo
33
+ ```
34
+
35
+ It creates three idempotent sample suggestions across pending, applied, and
36
+ rejected states. Running the task again refreshes those records instead of
37
+ duplicating them.
38
+
29
39
  Boot the app in development and look for the **"Suggest edits"** pill in the
30
40
  bottom-left. Click it, then click any text. `Esc` exits.
31
41
 
@@ -39,6 +49,12 @@ No layout change needed — the widget injects itself into HTML responses.
39
49
  Ruby >= 3.2 · Rails >= 7.1 · CSRF token comes from `csrf_meta_tags`, already in
40
50
  a standard Rails layout.
41
51
 
52
+ Installing with a coding agent? Point it at [AGENTS.md](AGENTS.md) — the same
53
+ steps in the order an agent needs them, the gates it tends to get wrong, and the
54
+ things it should not do (starting with: this gem never writes your locale files).
55
+ It ships inside the gem, so `cat "$(bundle show i18n_proofreading)/AGENTS.md"`
56
+ works from any app that bundles it.
57
+
42
58
  ## What you get
43
59
 
44
60
  | | |
@@ -89,6 +105,7 @@ Everything is optional — the defaults work out of the box in development. In
89
105
  | `enabled_environments` | `%w[development staging]` | Environments the tool is active in |
90
106
  | `enabled` | everyone | Extra per-request gate. `false` hides the tool |
91
107
  | `authorize_admin` | development only | **Who can open the review board.** Independent of the gates above |
108
+ | `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits — name your admin's and it adopts its layout, helpers and auth |
92
109
  | `current_user` | `nil` | Attribute a suggestion to a user. Receives the request |
93
110
  | `author_label` | the user's `email` | Label shown for the author |
94
111
  | `available_locales` | `I18n.available_locales` | Which locales a suggestion may target |
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ # Who is asking, and the gates that answer it.
5
+ #
6
+ # A concern rather than inherited behaviour because the engine has two
7
+ # controller roots: the public endpoints hang off ActionController::Base, and
8
+ # the dashboard hangs off whatever the host set as `base_controller_class`.
9
+ module RequestContext
10
+ extend ActiveSupport::Concern
11
+
12
+ private
13
+
14
+ def i18n_proofreading_admin_layout
15
+ I18nProofreading.config.admin_layout
16
+ end
17
+
18
+ # Gate for the widget's public API (submit a suggestion, read prior context).
19
+ # The client can set the cookie, but it can never reach the endpoints unless
20
+ # the app itself says the tool is available for this request.
21
+ def require_available
22
+ head :forbidden unless I18nProofreading.available?(request)
23
+ end
24
+
25
+ # Gate for the triage dashboard. Independent of #require_available (see
26
+ # I18nProofreading.admin?). Renders a plain 403 with a hint rather than a bare
27
+ # head, since a human is usually looking at it.
28
+ def require_admin
29
+ return if I18nProofreading.admin?(request)
30
+
31
+ render plain: 'Forbidden. Set I18nProofreading.config.authorize_admin to grant access.',
32
+ status: :forbidden
33
+ end
34
+
35
+ def current_author
36
+ return @current_author if defined?(@current_author)
37
+
38
+ @current_author = I18nProofreading.config.current_user.call(request)
39
+ end
40
+ end
41
+ end
@@ -1,36 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nProofreading
4
+ # Root of the engine's PUBLIC surface: widget.js, the context lookup and the
5
+ # suggestion endpoint. These stay on a plain ActionController::Base
6
+ # deliberately — a proofreader suggesting a translation must not be routed
7
+ # through a host's admin controller, which would demand a staff session.
8
+ #
9
+ # The dashboard's root is DashboardController, and that is where
10
+ # `config.base_controller_class` applies.
4
11
  class ApplicationController < ActionController::Base
5
- protect_from_forgery with: :exception
6
-
7
- private
8
-
9
- def i18n_proofreading_admin_layout
10
- I18nProofreading.config.admin_layout
11
- end
12
-
13
- # Gate for the widget's public API (submit a suggestion, read prior context).
14
- # The client can set the cookie, but it can never reach the endpoints unless
15
- # the app itself says the tool is available for this request.
16
- def require_available
17
- head :forbidden unless I18nProofreading.available?(request)
18
- end
12
+ include RequestContext
19
13
 
20
- # Gate for the triage dashboard. Independent of #require_available (see
21
- # I18nProofreading.admin?). Renders a plain 403 with a hint rather than a bare
22
- # head, since a human is usually looking at it.
23
- def require_admin
24
- return if I18nProofreading.admin?(request)
25
-
26
- render plain: 'Forbidden. Set I18nProofreading.config.authorize_admin to grant access.',
27
- status: :forbidden
28
- end
29
-
30
- def current_author
31
- return @current_author if defined?(@current_author)
32
-
33
- @current_author = I18nProofreading.config.current_user.call(request)
34
- end
14
+ protect_from_forgery with: :exception
35
15
  end
36
16
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ # Root of the STAFF surface: the suggestion triage dashboard.
5
+ #
6
+ # Inherits from `config.base_controller_class` — by default a plain
7
+ # ActionController::Base, which is why `authorize_admin` exists. Point it at
8
+ # the controller your own admin already inherits from and the dashboard picks
9
+ # up that stack wholesale: your layout, your helpers, your authentication, and
10
+ # whatever request context your before_actions establish.
11
+ #
12
+ # Only the dashboard hangs off it. The widget's endpoints stay on
13
+ # ApplicationController, so wiring an admin base controller here can never
14
+ # demand a staff session from someone suggesting a translation.
15
+ class DashboardController < I18nProofreading.base_controller
16
+ include RequestContext
17
+
18
+ # A host base controller brings its own layout, and declaring one here would
19
+ # override it. So the gem only claims the layout when it owns the decision:
20
+ # no host base controller, or a host that named an `admin_layout` explicitly.
21
+ layout :i18n_proofreading_admin_layout unless superclass != ActionController::Base &&
22
+ I18nProofreading.config.admin_layout == Configuration::DEFAULT_ADMIN_LAYOUT
23
+
24
+ before_action :require_admin
25
+
26
+ # A host base controller has configured CSRF already; declaring it twice
27
+ # would run the check twice.
28
+ protect_from_forgery with: :exception if superclass == ActionController::Base
29
+ end
30
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nProofreading
4
+ # The widget's public API: the context lookup it reads and the suggestion it
5
+ # posts.
6
+ #
7
+ # Public, so it stays on ApplicationController and never inherits a host's
8
+ # admin base controller — a proofreader must not be asked for a staff session.
9
+ # The triage actions live in SuggestionsController, which does inherit it.
10
+ class SubmissionsController < ApplicationController
11
+ before_action :require_available
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) && I18nProofreading.config.rate_limit
18
+ rate_limit(**I18nProofreading.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
+ # Pending suggestions for one key/locale, shown as read-only context when the
27
+ # proofreader reopens the popover for a string someone already flagged.
28
+ def context
29
+ suggestions = Suggestion
30
+ .where(translation_key: params[:key], locale: params[:locale])
31
+ .status_pending
32
+ .newest_first
33
+ .limit(20)
34
+
35
+ render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
36
+ end
37
+
38
+ def create
39
+ suggestion = Suggestion.new(suggestion_params)
40
+ attribute_author(suggestion)
41
+
42
+ if suggestion.save
43
+ I18nProofreading.config.on_submit.call(suggestion)
44
+ head :created
45
+ else
46
+ render json: { errors: suggestion.errors.full_messages }, status: :unprocessable_entity
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def attribute_author(suggestion)
53
+ author = current_author
54
+ return unless author
55
+
56
+ suggestion.author_id = author.id.to_s if author.respond_to?(:id)
57
+ suggestion.author_label = I18nProofreading.config.author_label.call(author)
58
+ end
59
+
60
+ def suggestion_json(suggestion)
61
+ {
62
+ proposed_value: suggestion.proposed_value,
63
+ author_label: suggestion.author_label,
64
+ created_at: suggestion.created_at.iso8601
65
+ }
66
+ end
67
+
68
+ def suggestion_params
69
+ params
70
+ .require(:suggestion)
71
+ .permit(:translation_key, :locale, :old_value, :proposed_value, :comment, :page_url)
72
+ end
73
+ end
74
+ end
@@ -1,29 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nProofreading
4
- class SuggestionsController < ApplicationController
4
+ class SuggestionsController < DashboardController
5
5
  PER_PAGE = 50
6
6
 
7
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: %i[index show]
10
8
  before_action :set_suggestion, only: :show
11
9
 
12
- layout :i18n_proofreading_admin_layout, only: %i[index show]
13
-
14
- # Throttle the public submission endpoint per IP so one user or bot can't
15
- # flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
16
- # Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
17
- # read once at boot, after the host's initializer.
18
- if respond_to?(:rate_limit) && I18nProofreading.config.rate_limit
19
- rate_limit(**I18nProofreading.config.rate_limit,
20
- only: :create,
21
- with: lambda {
22
- render json: { errors: ['Too many suggestions. Please slow down and try again.'] },
23
- status: :too_many_requests
24
- })
25
- end
26
-
27
10
  # --- triage dashboard (admin) --------------------------------------------
28
11
 
29
12
  def index
@@ -46,56 +29,10 @@ module I18nProofreading
46
29
 
47
30
  # --- widget API (public) -------------------------------------------------
48
31
 
49
- # Pending suggestions for one key/locale, shown as read-only context when the
50
- # proofreader reopens the popover for a string someone already flagged.
51
- def context
52
- suggestions = Suggestion
53
- .where(translation_key: params[:key], locale: params[:locale])
54
- .status_pending
55
- .newest_first
56
- .limit(20)
57
-
58
- render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
59
- end
60
-
61
- def create
62
- suggestion = Suggestion.new(suggestion_params)
63
- attribute_author(suggestion)
64
-
65
- if suggestion.save
66
- I18nProofreading.config.on_submit.call(suggestion)
67
- head :created
68
- else
69
- render json: { errors: suggestion.errors.full_messages }, status: :unprocessable_entity
70
- end
71
- end
72
-
73
32
  private
74
33
 
75
34
  def set_suggestion
76
35
  @suggestion = Suggestion.find(params[:id])
77
36
  end
78
-
79
- def attribute_author(suggestion)
80
- author = current_author
81
- return unless author
82
-
83
- suggestion.author_id = author.id.to_s if author.respond_to?(:id)
84
- suggestion.author_label = I18nProofreading.config.author_label.call(author)
85
- end
86
-
87
- def suggestion_json(suggestion)
88
- {
89
- proposed_value: suggestion.proposed_value,
90
- author_label: suggestion.author_label,
91
- created_at: suggestion.created_at.iso8601
92
- }
93
- end
94
-
95
- def suggestion_params
96
- params
97
- .require(:suggestion)
98
- .permit(:translation_key, :locale, :old_value, :proposed_value, :comment, :page_url)
99
- end
100
37
  end
101
38
  end
@@ -0,0 +1,13 @@
1
+ <%# The dashboard's own shell, rendered by the views rather than the layout.
2
+ That is deliberate: `config.admin_layout` and `config.base_controller_class`
3
+ both let a host replace the layout, and a stylesheet declared in a layout the
4
+ host replaces simply vanishes — the dashboard then renders unstyled.
5
+ Declaring it here means every layout works, host or gem.
6
+
7
+ The `ip-dashboard` wrapper is what scopes dashboard.css (see the comment at
8
+ the top of that file), so it has to enclose the content, not precede it. %>
9
+ <%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{I18nProofreading::Widget.dashboard_stylesheet_fingerprint}",
10
+ 'data-turbo-track': 'reload' %>
11
+ <div class="ip-dashboard">
12
+ <%= yield %>
13
+ </div>