i18n_feedback 0.3.1 → 0.5.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: b7fcc717fd98e35452b32dced0c863804d92a13c6c50986004c74ffad64189ca
4
- data.tar.gz: 00fa165a3e61bd18e773458cc00c7e38d324e3b00387b1eb0b78269738b73369
3
+ metadata.gz: 9fdbfd25f140436e323ceb8eec7f22aaa1aa52d4e9979cb0e6cddf4020bf53a9
4
+ data.tar.gz: a6e68e6e68cc021427164c95f671a012c78c31855aa9d1fef5c8cea528577ca1
5
5
  SHA512:
6
- metadata.gz: 8b2dcf16cb6edcec67828d8294cf670205c5b61b591cbeded1eb6605519a0a16b485d644c8b60d7b24d3134088396a22657a653acdeac7dc95b2aeeae64ea1ff
7
- data.tar.gz: 8a60071cfafc8fd341723f4d92f132caa2b71953de672bb3f508b2923315e698ccdac7431acf5f07724c330cb2a70223abb34d560b65e61aa40350875c754b18
6
+ metadata.gz: 822a81c0d8e8d08c2d6e03a9208e899a2a5a6649bbafe347b3e56c3ab6c430bfb059e05ec03abde8307c2a9692f08c64d5f9b7a0c94fe8c10eefead0385ca845
7
+ data.tar.gz: cd0d806e70019c47b00572bf16e64a13b6435337f36affc6a9f283df8949f56d575fcfe0d35b147d558112990c1e07a0ef9dda3b98808bede245fbd3feed2b8e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.0]
6
+
7
+ - Add a `status` to each suggestion — a string-backed Active Record enum with
8
+ values `pending`, `applied`, and `rejected` (`I18nFeedback::Suggestion::STATUSES`),
9
+ `status_`-prefixed (`status_applied?`, `status_applied!`, `Suggestion.status_pending`)
10
+ plus a `newest_first` scope. New suggestions start `pending`; the popover's
11
+ "already suggested" context now shows only pending ones, so applied/rejected
12
+ wordings stop resurfacing.
13
+
14
+ **Upgrading an existing install:** add the column with a migration —
15
+
16
+ ```ruby
17
+ add_column :i18n_feedback_suggestions, :status, :string, null: false, default: "pending"
18
+ add_index :i18n_feedback_suggestions, :status
19
+ ```
20
+
21
+ - Harden the public submission endpoint against abuse:
22
+ - Per-IP rate limiting via Rails' built-in limiter (Rails 7.2+; a no-op on
23
+ 7.1). Default 30 requests / 60s, tunable or disable-able via
24
+ `config.rate_limit`; returns `429 Too Many Requests`.
25
+ - Length caps on the stored free-text fields (`proposed_value` / `old_value`
26
+ 5000, `comment` / `page_url` 2000, `translation_key` 500) so a client can't
27
+ bloat the table with unbounded input.
28
+
29
+ - Add a `.github/workflows/release.yml` that publishes to RubyGems via trusted
30
+ publishing (OIDC) when a `v*` tag is pushed — no stored credentials or MFA
31
+ prompt.
32
+
33
+ ## [0.4.0]
34
+
35
+ - Add a `config.on_submit` hook, called with each saved suggestion right after
36
+ it's stored — notify Slack, send an email, open a ticket. Runs inline after
37
+ save, so keep it fast or hand off to a job.
38
+
5
39
  ## [0.3.1]
6
40
 
7
41
  - Fix the widget showing raw key markers (e.g. `⟦i18n_feedback.title⟧`) in its own
data/README.md CHANGED
@@ -116,6 +116,10 @@ I18nFeedback.configure do |config|
116
116
 
117
117
  # Keep in sync with the `mount` in config/routes.rb.
118
118
  config.mount_path = "/i18n_feedback"
119
+
120
+ # Per-IP throttle on the submission endpoint, passed to Rails' built-in rate
121
+ # limiter (Rails 7.2+; ignored on 7.1). Set nil to disable.
122
+ config.rate_limit = { to: 30, within: 60 }
119
123
  end
120
124
  ```
121
125
 
@@ -226,13 +230,35 @@ blue accent stays the same in both.
226
230
  Suggestions are ordinary records:
227
231
 
228
232
  ```ruby
229
- I18nFeedback::Suggestion.order(created_at: :desc).each do |s|
233
+ I18nFeedback::Suggestion.where(status: "pending").newest_first.each do |s|
230
234
  puts "#{s.locale} #{s.translation_key}: #{s.old_value.inspect} -> #{s.proposed_value.inspect}"
231
235
  end
232
236
  ```
233
237
 
234
238
  Each row stores `translation_key`, `locale`, `old_value`, `proposed_value`,
235
- `comment`, `page_url`, and optional `author_id` / `author_label`.
239
+ `comment`, `page_url`, `status`, and optional `author_id` / `author_label`.
240
+
241
+ Every suggestion has a `status` — one of `pending`, `applied`, or `rejected`
242
+ (`I18nFeedback::Suggestion::STATUSES`), backed by an Active Record enum. New
243
+ suggestions start `pending`; once you apply a wording to your locale files or
244
+ decide against it, set the status accordingly so the popover stops offering it
245
+ as pending context:
246
+
247
+ ```ruby
248
+ suggestion.status_applied! # bang setter
249
+ suggestion.status_applied? # => true
250
+ I18nFeedback::Suggestion.status_pending.newest_first # scope per status
251
+ ```
252
+
253
+ ### Getting notified
254
+
255
+ To be pinged when a suggestion comes in, set `on_submit`. It's called with the
256
+ saved `Suggestion` right after it's stored — notify Slack, send an email, open a
257
+ ticket. It runs inline in the request, so keep it fast or hand off to a job:
258
+
259
+ ```ruby
260
+ config.on_submit = ->(suggestion) { SuggestionMailer.with(suggestion:).created.deliver_later }
261
+ ```
236
262
 
237
263
  ## Security
238
264
 
data/Rakefile CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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'
4
7
 
5
8
  APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
9
  load 'rails/tasks/engine.rake' if File.exist?(APP_RAKEFILE)
@@ -2,12 +2,26 @@
2
2
 
3
3
  module I18nFeedback
4
4
  class SuggestionsController < ApplicationController
5
+ # Throttle the public submission endpoint per IP so one user or bot can't
6
+ # flood the table. Uses the rate limiter built into Rails 7.2+ (backed by
7
+ # Rails.cache); a no-op on Rails 7.1. Tune or disable via config.rate_limit —
8
+ # read once at boot, after the host's initializer.
9
+ if respond_to?(:rate_limit) && I18nFeedback.config.rate_limit
10
+ rate_limit(**I18nFeedback.config.rate_limit,
11
+ only: :create,
12
+ with: lambda {
13
+ render json: { errors: ['Too many suggestions. Please slow down and try again.'] },
14
+ status: :too_many_requests
15
+ })
16
+ end
17
+
5
18
  # Pending suggestions for one key/locale, shown as read-only context when the
6
19
  # proofreader reopens the popover for a string someone already flagged.
7
20
  def index
8
21
  suggestions = Suggestion
9
22
  .where(translation_key: params[:key], locale: params[:locale])
10
- .order(id: :desc)
23
+ .status_pending
24
+ .newest_first
11
25
  .limit(20)
12
26
 
13
27
  render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
@@ -18,6 +32,7 @@ module I18nFeedback
18
32
  attribute_author(suggestion)
19
33
 
20
34
  if suggestion.save
35
+ I18nFeedback.config.on_submit.call(suggestion)
21
36
  head :created
22
37
  else
23
38
  render json: { errors: suggestion.errors.full_messages }, status: :unprocessable_entity
@@ -5,10 +5,23 @@ module I18nFeedback
5
5
  # is optional and stored as loose fields (no foreign key to the host's user
6
6
  # table) so the model is portable across apps with different user models.
7
7
  class Suggestion < ApplicationRecord
8
- validates :translation_key, presence: true
9
- validates :proposed_value, presence: true
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 }
10
21
  validates :locale,
11
22
  presence: true,
12
23
  inclusion: { in: ->(_) { I18nFeedback.config.available_locales.call.map(&:to_s) } }
24
+
25
+ scope :newest_first, -> { order(id: :desc) }
13
26
  end
14
27
  end
@@ -9,6 +9,7 @@ class CreateI18nFeedbackSuggestions < ActiveRecord::Migration<%= migration_versi
9
9
  t.text :proposed_value, null: false
10
10
  t.text :comment
11
11
  t.string :page_url
12
+ t.string :status, null: false, default: 'pending'
12
13
  t.string :author_id
13
14
  t.string :author_label
14
15
 
@@ -16,5 +17,6 @@ class CreateI18nFeedbackSuggestions < ActiveRecord::Migration<%= migration_versi
16
17
  end
17
18
 
18
19
  add_index :i18n_feedback_suggestions, %i[translation_key locale]
20
+ add_index :i18n_feedback_suggestions, :status
19
21
  end
20
22
  end
@@ -36,4 +36,9 @@ I18nFeedback.configure do |config|
36
36
  # Keep this in sync with the `mount` line in config/routes.rb.
37
37
  #
38
38
  # config.mount_path = "/i18n_feedback"
39
+
40
+ # Called with each saved suggestion — notify Slack, send an email, open a
41
+ # ticket. Runs inline after save, so keep it fast or hand off to a job.
42
+ #
43
+ # config.on_submit = ->(suggestion) { SuggestionMailer.with(suggestion:).created.deliver_later }
39
44
  end
@@ -48,6 +48,15 @@ module I18nFeedback
48
48
  # suggest mode without the parameter.
49
49
  attr_accessor :toggle_param
50
50
 
51
+ # Called with each saved suggestion — notify Slack, send an email, open a
52
+ # ticket. Runs inline after save; keep it fast or hand off to a job.
53
+ attr_accessor :on_submit
54
+
55
+ # Per-IP throttle for the public submission endpoint, as keyword arguments
56
+ # for Rails' rate limiter (Rails 7.2+; ignored on 7.1). Read once when the
57
+ # controller loads — set it in an initializer. nil disables throttling.
58
+ attr_accessor :rate_limit
59
+
51
60
  def initialize
52
61
  @enabled_environments = %w[development staging]
53
62
  @enabled = ->(_request) { true }
@@ -59,6 +68,8 @@ module I18nFeedback
59
68
  @show_pill = true
60
69
  @pill_label = nil
61
70
  @toggle_param = 'i18n_feedback'
71
+ @on_submit = ->(_suggestion) {}
72
+ @rate_limit = { to: 30, within: 60 }
62
73
  end
63
74
 
64
75
  def environment_enabled?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nFeedback
4
- VERSION = '0.3.1'
4
+ VERSION = '0.5.0'
5
5
  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.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -97,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
97
  - !ruby/object:Gem::Version
98
98
  version: '0'
99
99
  requirements: []
100
- rubygems_version: 4.0.6
100
+ rubygems_version: 3.6.9
101
101
  specification_version: 4
102
102
  summary: 'In-context i18n proofreading for Rails: click any translated string and
103
103
  suggest a fix.'