i18n_feedback 0.4.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +31 -9
- data/Rakefile +3 -0
- data/app/controllers/i18n_feedback/suggestions_controller.rb +15 -1
- data/app/models/i18n_feedback/suggestion.rb +15 -2
- data/config/locales/i18n_feedback.ar.yml +2 -0
- data/config/locales/i18n_feedback.bn.yml +2 -0
- data/config/locales/i18n_feedback.de.yml +2 -0
- data/config/locales/i18n_feedback.en.yml +2 -0
- data/config/locales/i18n_feedback.es.yml +2 -0
- data/config/locales/i18n_feedback.fr.yml +2 -0
- data/config/locales/i18n_feedback.hi.yml +2 -0
- data/config/locales/i18n_feedback.id.yml +2 -0
- data/config/locales/i18n_feedback.it.yml +2 -0
- data/config/locales/i18n_feedback.ja.yml +2 -0
- data/config/locales/i18n_feedback.ko.yml +2 -0
- data/config/locales/i18n_feedback.nl.yml +2 -0
- data/config/locales/i18n_feedback.pl.yml +2 -0
- data/config/locales/i18n_feedback.pt.yml +2 -0
- data/config/locales/i18n_feedback.ru.yml +2 -0
- data/config/locales/i18n_feedback.th.yml +2 -0
- data/config/locales/i18n_feedback.tr.yml +2 -0
- data/config/locales/i18n_feedback.uk.yml +2 -0
- data/config/locales/i18n_feedback.ur.yml +2 -0
- data/config/locales/i18n_feedback.vi.yml +2 -0
- data/config/locales/i18n_feedback.zh-CN.yml +2 -0
- data/lib/generators/i18n_feedback/install/templates/create_i18n_feedback_suggestions.rb.tt +2 -0
- data/lib/generators/i18n_feedback/install/templates/initializer.rb +5 -0
- data/lib/i18n_feedback/configuration.rb +6 -0
- data/lib/i18n_feedback/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef2003dd6566b01a576e74b7db0ef2e277e71a642c18e571683e8509db7af1b6
|
|
4
|
+
data.tar.gz: ea5121783f2dec044550682d9be5cbcdf5c1f8990bef886ec7a04ac414463f26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02fba91e18f211348b3c99a23de1c0ca6b4bdc22c63a7cf7833c537cfc2a671e9802e76ff2385687135b410d574d70a5ae57f38a8e770d0bc70b351946f85d7e
|
|
7
|
+
data.tar.gz: e30815c01401a293b4226e2fd9f6cfbc39dbb660924520c3325fef0954f786791346f8160d06b96b5f479d0d8f034b4c4bdb80ce1f33cbfb68f4d56bd4beeb3f
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.0]
|
|
6
|
+
|
|
7
|
+
- Ship `i18n_feedback.start` / `i18n_feedback.stop` labels in every bundled
|
|
8
|
+
language, for hosts that drive suggest mode from their own menu/link instead
|
|
9
|
+
of the floating pill. Because the `i18n_feedback.*` scope is exempt from
|
|
10
|
+
key-marking, these are safe to render with `t(...)` — no plain-literal
|
|
11
|
+
workaround needed. See the "Toggling suggest mode from your own link" README
|
|
12
|
+
section for a full HTML example.
|
|
13
|
+
|
|
14
|
+
## [0.5.0]
|
|
15
|
+
|
|
16
|
+
- Add a `status` to each suggestion — a string-backed Active Record enum with
|
|
17
|
+
values `pending`, `applied`, and `rejected` (`I18nFeedback::Suggestion::STATUSES`),
|
|
18
|
+
`status_`-prefixed (`status_applied?`, `status_applied!`, `Suggestion.status_pending`)
|
|
19
|
+
plus a `newest_first` scope. New suggestions start `pending`; the popover's
|
|
20
|
+
"already suggested" context now shows only pending ones, so applied/rejected
|
|
21
|
+
wordings stop resurfacing.
|
|
22
|
+
|
|
23
|
+
**Upgrading an existing install:** add the column with a migration —
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
add_column :i18n_feedback_suggestions, :status, :string, null: false, default: "pending"
|
|
27
|
+
add_index :i18n_feedback_suggestions, :status
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Harden the public submission endpoint against abuse:
|
|
31
|
+
- Per-IP rate limiting via Rails' built-in limiter (Rails 7.2+; a no-op on
|
|
32
|
+
7.1). Default 30 requests / 60s, tunable or disable-able via
|
|
33
|
+
`config.rate_limit`; returns `429 Too Many Requests`.
|
|
34
|
+
- Length caps on the stored free-text fields (`proposed_value` / `old_value`
|
|
35
|
+
5000, `comment` / `page_url` 2000, `translation_key` 500) so a client can't
|
|
36
|
+
bloat the table with unbounded input.
|
|
37
|
+
|
|
38
|
+
- Add a `.github/workflows/release.yml` that publishes to RubyGems via trusted
|
|
39
|
+
publishing (OIDC) when a `v*` tag is pushed — no stored credentials or MFA
|
|
40
|
+
prompt.
|
|
41
|
+
|
|
5
42
|
## [0.4.0]
|
|
6
43
|
|
|
7
44
|
- Add a `config.on_submit` hook, called with each saved suggestion right after
|
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
|
|
|
@@ -132,16 +136,20 @@ config.show_pill = false # optional
|
|
|
132
136
|
A one-way "turn it on" link is just the toggle parameter:
|
|
133
137
|
|
|
134
138
|
```erb
|
|
135
|
-
<%= link_to "
|
|
139
|
+
<%= link_to t("i18n_feedback.start"), "?i18n_feedback=true" %>
|
|
136
140
|
```
|
|
137
141
|
|
|
138
142
|
For a single control that flips both ways, read the current state from the
|
|
139
|
-
`i18n_feedback` cookie and point at the opposite state
|
|
143
|
+
`i18n_feedback` cookie and point at the opposite state. The gem ships `start`
|
|
144
|
+
and `stop` labels under the `i18n_feedback.*` scope in every bundled language,
|
|
145
|
+
so a localized toggle needs no keys of your own:
|
|
140
146
|
|
|
141
147
|
```erb
|
|
142
|
-
<%
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
<% if I18nFeedback.available?(request) %>
|
|
149
|
+
<% suggesting = cookies[:i18n_feedback].present? %>
|
|
150
|
+
<%= link_to (suggesting ? t("i18n_feedback.stop") : t("i18n_feedback.start")),
|
|
151
|
+
"?#{I18nFeedback.config.toggle_param}=#{!suggesting}" %>
|
|
152
|
+
<% end %>
|
|
145
153
|
```
|
|
146
154
|
|
|
147
155
|
Good to know:
|
|
@@ -154,8 +162,10 @@ Good to know:
|
|
|
154
162
|
ordinary navigation during proofreading (so a stray click can't leave the page
|
|
155
163
|
mid-edit), but any link carrying the toggle parameter is exempt — so a "Disable"
|
|
156
164
|
item in your nav always gets you out.
|
|
157
|
-
- **
|
|
158
|
-
|
|
165
|
+
- The `i18n_feedback.*` keys are **safe to run through `I18n.t`** — that scope is
|
|
166
|
+
exempt from key-marking, so the tool never flags its own controls as editable.
|
|
167
|
+
Use the bundled `i18n_feedback.start` / `i18n_feedback.stop` labels (or your own
|
|
168
|
+
keys); either way, no plain-literal workaround is needed.
|
|
159
169
|
|
|
160
170
|
### Gating examples
|
|
161
171
|
|
|
@@ -226,13 +236,25 @@ blue accent stays the same in both.
|
|
|
226
236
|
Suggestions are ordinary records:
|
|
227
237
|
|
|
228
238
|
```ruby
|
|
229
|
-
I18nFeedback::Suggestion.
|
|
239
|
+
I18nFeedback::Suggestion.where(status: "pending").newest_first.each do |s|
|
|
230
240
|
puts "#{s.locale} #{s.translation_key}: #{s.old_value.inspect} -> #{s.proposed_value.inspect}"
|
|
231
241
|
end
|
|
232
242
|
```
|
|
233
243
|
|
|
234
244
|
Each row stores `translation_key`, `locale`, `old_value`, `proposed_value`,
|
|
235
|
-
`comment`, `page_url`, and optional `author_id` / `author_label`.
|
|
245
|
+
`comment`, `page_url`, `status`, and optional `author_id` / `author_label`.
|
|
246
|
+
|
|
247
|
+
Every suggestion has a `status` — one of `pending`, `applied`, or `rejected`
|
|
248
|
+
(`I18nFeedback::Suggestion::STATUSES`), backed by an Active Record enum. New
|
|
249
|
+
suggestions start `pending`; once you apply a wording to your locale files or
|
|
250
|
+
decide against it, set the status accordingly so the popover stops offering it
|
|
251
|
+
as pending context:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
suggestion.status_applied! # bang setter
|
|
255
|
+
suggestion.status_applied? # => true
|
|
256
|
+
I18nFeedback::Suggestion.status_pending.newest_first # scope per status
|
|
257
|
+
```
|
|
236
258
|
|
|
237
259
|
### Getting notified
|
|
238
260
|
|
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
|
-
.
|
|
23
|
+
.status_pending
|
|
24
|
+
.newest_first
|
|
11
25
|
.limit(20)
|
|
12
26
|
|
|
13
27
|
render json: suggestions.map { |suggestion| suggestion_json(suggestion) }
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
|
@@ -41,4 +41,9 @@ I18nFeedback.configure do |config|
|
|
|
41
41
|
# ticket. Runs inline after save, so keep it fast or hand off to a job.
|
|
42
42
|
#
|
|
43
43
|
# config.on_submit = ->(suggestion) { SuggestionMailer.with(suggestion:).created.deliver_later }
|
|
44
|
+
|
|
45
|
+
# Per-IP throttle on the submission endpoint, passed to Rails' built-in rate
|
|
46
|
+
# limiter (Rails 7.2+; ignored on 7.1). Set nil to disable.
|
|
47
|
+
#
|
|
48
|
+
# config.rate_limit = { to: 30, within: 60 }
|
|
44
49
|
end
|
|
@@ -52,6 +52,11 @@ module I18nFeedback
|
|
|
52
52
|
# ticket. Runs inline after save; keep it fast or hand off to a job.
|
|
53
53
|
attr_accessor :on_submit
|
|
54
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
|
+
|
|
55
60
|
def initialize
|
|
56
61
|
@enabled_environments = %w[development staging]
|
|
57
62
|
@enabled = ->(_request) { true }
|
|
@@ -64,6 +69,7 @@ module I18nFeedback
|
|
|
64
69
|
@pill_label = nil
|
|
65
70
|
@toggle_param = 'i18n_feedback'
|
|
66
71
|
@on_submit = ->(_suggestion) {}
|
|
72
|
+
@rate_limit = { to: 30, within: 60 }
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
def environment_enabled?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n_feedback
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.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:
|
|
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.'
|