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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4dee71413e641b59a88d1116ac134410395055d31f097d83800155feead9fc8
4
- data.tar.gz: 4e41cd76dd83acbdd77ff95ece48bed916040bdf0e6227e78aa6090f8c28025b
3
+ metadata.gz: ef2003dd6566b01a576e74b7db0ef2e277e71a642c18e571683e8509db7af1b6
4
+ data.tar.gz: ea5121783f2dec044550682d9be5cbcdf5c1f8990bef886ec7a04ac414463f26
5
5
  SHA512:
6
- metadata.gz: 9483d2a561d1edb370685c85ac3dfaa5030532c58102aeb923926ea180de687d738e9e7deccbf8a6689f617f4ae7da7a20750272a5306419d60389d574c5b4f4
7
- data.tar.gz: d50924f6b93eec7fe93bb5e5baa8a7a53a43e8fdcf864b571531646d3f47f8554dff87e6766289ce0589830f0d354176759ea85ebadbff6722396664aec632b9
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 "Proofread translations", "?i18n_feedback=true" %>
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
- <% on = cookies[:i18n_feedback].present? %>
143
- <%= link_to (on ? "Disable translations editing" : "Enable translations editing"),
144
- "?i18n_feedback=#{!on}" %>
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
- - **Don't** run the label through `I18n.t`: the tool would then mark its own
158
- control as an editable string. Keep the label a plain literal.
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.order(created_at: :desc).each do |s|
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
- .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) }
@@ -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
@@ -1,6 +1,8 @@
1
1
  ar:
2
2
  i18n_feedback:
3
3
  pill: "اقترح تعديلات"
4
+ start: "اقترح تعديلات"
5
+ stop: "إيقاف الاقتراح"
4
6
  pill_active: "وضع الاقتراح — اضغط للخروج (Esc)"
5
7
  title: "اقترح تصحيحًا للترجمة"
6
8
  current_text: "النص الحالي"
@@ -1,6 +1,8 @@
1
1
  bn:
2
2
  i18n_feedback:
3
3
  pill: "সম্পাদনা প্রস্তাব করুন"
4
+ start: "সম্পাদনা প্রস্তাব করুন"
5
+ stop: "প্রস্তাব দেওয়া বন্ধ করুন"
4
6
  pill_active: "প্রস্তাব চলছে — বের হতে ট্যাপ করুন (Esc)"
5
7
  title: "অনুবাদ সংশোধন প্রস্তাব করুন"
6
8
  current_text: "বর্তমান লেখা"
@@ -1,6 +1,8 @@
1
1
  de:
2
2
  i18n_feedback:
3
3
  pill: "Änderungen vorschlagen"
4
+ start: "Änderungen vorschlagen"
5
+ stop: "Vorschläge beenden"
4
6
  pill_active: "Vorschlagsmodus — zum Beenden tippen (Esc)"
5
7
  title: "Übersetzungskorrektur vorschlagen"
6
8
  current_text: "Aktueller Text"
@@ -1,6 +1,8 @@
1
1
  en:
2
2
  i18n_feedback:
3
3
  pill: "Suggest edits"
4
+ start: "Suggest edits"
5
+ stop: "Stop suggesting"
4
6
  pill_active: "Suggesting — tap to exit (Esc)"
5
7
  title: "Suggest a translation fix"
6
8
  current_text: "Current text"
@@ -1,6 +1,8 @@
1
1
  es:
2
2
  i18n_feedback:
3
3
  pill: "Sugerir cambios"
4
+ start: "Sugerir cambios"
5
+ stop: "Dejar de sugerir"
4
6
  pill_active: "Sugiriendo — toca para salir (Esc)"
5
7
  title: "Sugerir una corrección de traducción"
6
8
  current_text: "Texto actual"
@@ -1,6 +1,8 @@
1
1
  fr:
2
2
  i18n_feedback:
3
3
  pill: "Proposer des corrections"
4
+ start: "Proposer des corrections"
5
+ stop: "Arrêter de proposer"
4
6
  pill_active: "En cours — appuyez pour quitter (Échap)"
5
7
  title: "Proposer une correction de traduction"
6
8
  current_text: "Texte actuel"
@@ -1,6 +1,8 @@
1
1
  hi:
2
2
  i18n_feedback:
3
3
  pill: "बदलाव सुझाएँ"
4
+ start: "बदलाव सुझाएँ"
5
+ stop: "सुझाव देना बंद करें"
4
6
  pill_active: "सुझाव मोड — बाहर निकलने के लिए टैप करें (Esc)"
5
7
  title: "अनुवाद सुधार सुझाएँ"
6
8
  current_text: "वर्तमान टेक्स्ट"
@@ -1,6 +1,8 @@
1
1
  id:
2
2
  i18n_feedback:
3
3
  pill: "Sarankan perubahan"
4
+ start: "Sarankan perubahan"
5
+ stop: "Berhenti menyarankan"
4
6
  pill_active: "Menyarankan — ketuk untuk keluar (Esc)"
5
7
  title: "Sarankan perbaikan terjemahan"
6
8
  current_text: "Teks saat ini"
@@ -1,6 +1,8 @@
1
1
  it:
2
2
  i18n_feedback:
3
3
  pill: "Suggerisci modifiche"
4
+ start: "Suggerisci modifiche"
5
+ stop: "Interrompi i suggerimenti"
4
6
  pill_active: "Suggerimento — tocca per uscire (Esc)"
5
7
  title: "Suggerisci una correzione di traduzione"
6
8
  current_text: "Testo attuale"
@@ -1,6 +1,8 @@
1
1
  ja:
2
2
  i18n_feedback:
3
3
  pill: "修正を提案"
4
+ start: "修正を提案"
5
+ stop: "提案を終了"
4
6
  pill_active: "提案中 — タップで終了(Esc)"
5
7
  title: "翻訳の修正を提案"
6
8
  current_text: "現在のテキスト"
@@ -1,6 +1,8 @@
1
1
  ko:
2
2
  i18n_feedback:
3
3
  pill: "수정 제안"
4
+ start: "수정 제안"
5
+ stop: "제안 중지"
4
6
  pill_active: "제안 중 — 종료하려면 탭하세요 (Esc)"
5
7
  title: "번역 수정 제안"
6
8
  current_text: "현재 텍스트"
@@ -1,6 +1,8 @@
1
1
  nl:
2
2
  i18n_feedback:
3
3
  pill: "Wijzigingen voorstellen"
4
+ start: "Wijzigingen voorstellen"
5
+ stop: "Stoppen met voorstellen"
4
6
  pill_active: "Aan het voorstellen — tik om te sluiten (Esc)"
5
7
  title: "Een vertaalcorrectie voorstellen"
6
8
  current_text: "Huidige tekst"
@@ -1,6 +1,8 @@
1
1
  pl:
2
2
  i18n_feedback:
3
3
  pill: "Zaproponuj zmiany"
4
+ start: "Zaproponuj zmiany"
5
+ stop: "Zakończ sugerowanie"
4
6
  pill_active: "Tryb sugestii — dotknij, aby wyjść (Esc)"
5
7
  title: "Zaproponuj poprawkę tłumaczenia"
6
8
  current_text: "Bieżący tekst"
@@ -1,6 +1,8 @@
1
1
  pt:
2
2
  i18n_feedback:
3
3
  pill: "Sugerir edições"
4
+ start: "Sugerir edições"
5
+ stop: "Parar de sugerir"
4
6
  pill_active: "Sugerindo — toque para sair (Esc)"
5
7
  title: "Sugerir uma correção de tradução"
6
8
  current_text: "Texto atual"
@@ -1,6 +1,8 @@
1
1
  ru:
2
2
  i18n_feedback:
3
3
  pill: "Предложить правки"
4
+ start: "Предложить правки"
5
+ stop: "Остановить предложения"
4
6
  pill_active: "Режим предложений — нажмите, чтобы выйти (Esc)"
5
7
  title: "Предложить исправление перевода"
6
8
  current_text: "Текущий текст"
@@ -1,6 +1,8 @@
1
1
  th:
2
2
  i18n_feedback:
3
3
  pill: "แนะนำการแก้ไข"
4
+ start: "แนะนำการแก้ไข"
5
+ stop: "หยุดแนะนำ"
4
6
  pill_active: "กำลังแนะนำ — แตะเพื่อออก (Esc)"
5
7
  title: "แนะนำการแก้ไขคำแปล"
6
8
  current_text: "ข้อความปัจจุบัน"
@@ -1,6 +1,8 @@
1
1
  tr:
2
2
  i18n_feedback:
3
3
  pill: "Düzenleme öner"
4
+ start: "Düzenleme öner"
5
+ stop: "Önermeyi durdur"
4
6
  pill_active: "Öneri modu — çıkmak için dokun (Esc)"
5
7
  title: "Çeviri düzeltmesi öner"
6
8
  current_text: "Mevcut metin"
@@ -1,6 +1,8 @@
1
1
  uk:
2
2
  i18n_feedback:
3
3
  pill: "Запропонувати зміни"
4
+ start: "Запропонувати зміни"
5
+ stop: "Зупинити пропозиції"
4
6
  pill_active: "Режим пропозицій — торкніться, щоб вийти (Esc)"
5
7
  title: "Запропонувати виправлення перекладу"
6
8
  current_text: "Поточний текст"
@@ -1,6 +1,8 @@
1
1
  ur:
2
2
  i18n_feedback:
3
3
  pill: "ترامیم تجویز کریں"
4
+ start: "ترامیم تجویز کریں"
5
+ stop: "تجویز کرنا بند کریں"
4
6
  pill_active: "تجویز موڈ — باہر نکلنے کے لیے تھپتھپائیں (Esc)"
5
7
  title: "ترجمے کی اصلاح تجویز کریں"
6
8
  current_text: "موجودہ متن"
@@ -1,6 +1,8 @@
1
1
  vi:
2
2
  i18n_feedback:
3
3
  pill: "Đề xuất chỉnh sửa"
4
+ start: "Đề xuất chỉnh sửa"
5
+ stop: "Dừng đề xuất"
4
6
  pill_active: "Đang đề xuất — chạm để thoát (Esc)"
5
7
  title: "Đề xuất sửa bản dịch"
6
8
  current_text: "Văn bản hiện tại"
@@ -1,6 +1,8 @@
1
1
  zh-CN:
2
2
  i18n_feedback:
3
3
  pill: "建议修改"
4
+ start: "建议修改"
5
+ stop: "停止建议"
4
6
  pill_active: "建议中 — 点击退出(Esc)"
5
7
  title: "建议翻译修正"
6
8
  current_text: "当前文本"
@@ -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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nFeedback
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.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.4.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: 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.'