i18n_feedback 0.3.0 → 0.4.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 +20 -0
- data/README.md +12 -1
- data/app/controllers/i18n_feedback/suggestions_controller.rb +1 -0
- data/lib/generators/i18n_feedback/install/templates/initializer.rb +5 -0
- data/lib/i18n_feedback/configuration.rb +5 -0
- data/lib/i18n_feedback/marking.rb +5 -2
- data/lib/i18n_feedback/middleware.rb +19 -1
- data/lib/i18n_feedback/version.rb +1 -1
- data/lib/i18n_feedback/widget.rb +27 -19
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4dee71413e641b59a88d1116ac134410395055d31f097d83800155feead9fc8
|
|
4
|
+
data.tar.gz: 4e41cd76dd83acbdd77ff95ece48bed916040bdf0e6227e78aa6090f8c28025b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9483d2a561d1edb370685c85ac3dfaa5030532c58102aeb923926ea180de687d738e9e7deccbf8a6689f617f4ae7da7a20750272a5306419d60389d574c5b4f4
|
|
7
|
+
data.tar.gz: d50924f6b93eec7fe93bb5e5baa8a7a53a43e8fdcf864b571531646d3f47f8554dff87e6766289ce0589830f0d354176759ea85ebadbff6722396664aec632b9
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0]
|
|
6
|
+
|
|
7
|
+
- Add a `config.on_submit` hook, called with each saved suggestion right after
|
|
8
|
+
it's stored — notify Slack, send an email, open a ticket. Runs inline after
|
|
9
|
+
save, so keep it fast or hand off to a job.
|
|
10
|
+
|
|
11
|
+
## [0.3.1]
|
|
12
|
+
|
|
13
|
+
- Fix the widget showing raw key markers (e.g. `⟦i18n_feedback.title⟧`) in its own
|
|
14
|
+
popover while suggest mode was on. The key-marking backend was tagging the
|
|
15
|
+
tool's own `i18n_feedback.*` strings; those are not part of the host app's
|
|
16
|
+
translatable copy, so they're now always skipped — the widget never marks or
|
|
17
|
+
offers to edit its own UI.
|
|
18
|
+
- Fix the popover not following the page's language under auto-injection. The
|
|
19
|
+
widget is injected in middleware *after* the controller action, so an
|
|
20
|
+
`around_action { I18n.with_locale(...) }` had already reset `I18n.locale` back
|
|
21
|
+
to the default — the popover (and the saved suggestion's `locale`) came out in
|
|
22
|
+
the wrong language. The locale is now read from the page's rendered
|
|
23
|
+
`<html lang>` attribute and labels resolve under it explicitly.
|
|
24
|
+
|
|
5
25
|
## [0.3.0]
|
|
6
26
|
|
|
7
27
|
- Localize the widget's own UI. Every string in the pill and the suggestion
|
data/README.md
CHANGED
|
@@ -181,7 +181,8 @@ It renders nothing unless the tool is available for the request.
|
|
|
181
181
|
|
|
182
182
|
The pill and the suggestion popover speak the app's language: every string
|
|
183
183
|
resolves through Rails I18n under the `i18n_feedback.*` scope and follows the
|
|
184
|
-
|
|
184
|
+
language the page was rendered in (its `<html lang>`, falling back to
|
|
185
|
+
`I18n.locale`). Translations ship out of the box for English plus 20 more
|
|
185
186
|
languages — Arabic, Bengali, Chinese (Simplified), Dutch, French, German, Hindi,
|
|
186
187
|
Indonesian, Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish,
|
|
187
188
|
Thai, Turkish, Ukrainian, Urdu and Vietnamese — so the tool is already localized
|
|
@@ -233,6 +234,16 @@ end
|
|
|
233
234
|
Each row stores `translation_key`, `locale`, `old_value`, `proposed_value`,
|
|
234
235
|
`comment`, `page_url`, and optional `author_id` / `author_label`.
|
|
235
236
|
|
|
237
|
+
### Getting notified
|
|
238
|
+
|
|
239
|
+
To be pinged when a suggestion comes in, set `on_submit`. It's called with the
|
|
240
|
+
saved `Suggestion` right after it's stored — notify Slack, send an email, open a
|
|
241
|
+
ticket. It runs inline in the request, so keep it fast or hand off to a job:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
config.on_submit = ->(suggestion) { SuggestionMailer.with(suggestion:).created.deliver_later }
|
|
245
|
+
```
|
|
246
|
+
|
|
236
247
|
## Security
|
|
237
248
|
|
|
238
249
|
- The tool is gated **on the server** for every marker, endpoint, and injection.
|
|
@@ -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,10 @@ 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
|
+
|
|
51
55
|
def initialize
|
|
52
56
|
@enabled_environments = %w[development staging]
|
|
53
57
|
@enabled = ->(_request) { true }
|
|
@@ -59,6 +63,7 @@ module I18nFeedback
|
|
|
59
63
|
@show_pill = true
|
|
60
64
|
@pill_label = nil
|
|
61
65
|
@toggle_param = 'i18n_feedback'
|
|
66
|
+
@on_submit = ->(_suggestion) {}
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
def environment_enabled?
|
|
@@ -11,8 +11,11 @@ module I18nFeedback
|
|
|
11
11
|
RIGHT = '⟧'
|
|
12
12
|
|
|
13
13
|
# Skip format/lookup namespaces — marking these would corrupt number, date,
|
|
14
|
-
# and currency formatting and interpolation strings.
|
|
15
|
-
|
|
14
|
+
# and currency formatting and interpolation strings. The tool's own
|
|
15
|
+
# `i18n_feedback.*` strings are skipped too: they aren't part of the host
|
|
16
|
+
# app's translatable copy, so the widget must never mark (or offer to edit)
|
|
17
|
+
# its own UI.
|
|
18
|
+
SKIP = /(\A|\.)(number|date|time|datetime|support|i18n_feedback)(\.|\z)|formats?\z/
|
|
16
19
|
|
|
17
20
|
class << self
|
|
18
21
|
def enabled?
|
|
@@ -101,6 +101,24 @@ module I18nFeedback
|
|
|
101
101
|
content_type(headers).to_s.include?('text/html')
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# The locale the page was actually rendered in, read from its `<html lang>`
|
|
105
|
+
# attribute. Auto-injection runs after the controller action, so I18n.locale
|
|
106
|
+
# may already have been reset from whatever the request used — the rendered
|
|
107
|
+
# `lang` is the reliable record of the page's language. Falls back to the
|
|
108
|
+
# ambient locale when the page declares none.
|
|
109
|
+
def page_locale(html)
|
|
110
|
+
lang = html[/<html[^>]*\blang=["']([^"']+)["']/i, 1]
|
|
111
|
+
(lang && normalize_locale(lang)) || I18n.locale
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Map an HTML lang value ("es", "pt-BR") to a locale the app actually offers,
|
|
115
|
+
# trying the value as-is, its underscore form, then its language subtag. nil
|
|
116
|
+
# if none match, so the caller can fall back rather than emit an unknown one.
|
|
117
|
+
def normalize_locale(lang)
|
|
118
|
+
available = I18n.available_locales.map(&:to_s)
|
|
119
|
+
[lang, lang.tr('-', '_'), lang.split(/[-_]/).first].uniq.find { |c| available.include?(c) }&.to_sym
|
|
120
|
+
end
|
|
121
|
+
|
|
104
122
|
def content_type(headers)
|
|
105
123
|
headers['Content-Type'] || headers['content-type']
|
|
106
124
|
end
|
|
@@ -112,7 +130,7 @@ module I18nFeedback
|
|
|
112
130
|
|
|
113
131
|
snippet = Widget.snippet(
|
|
114
132
|
endpoint: I18nFeedback.config.suggestions_endpoint,
|
|
115
|
-
locale:
|
|
133
|
+
locale: page_locale(html),
|
|
116
134
|
active: marking,
|
|
117
135
|
nonce: nonce
|
|
118
136
|
)
|
data/lib/i18n_feedback/widget.rb
CHANGED
|
@@ -38,7 +38,7 @@ module I18nFeedback
|
|
|
38
38
|
showPill: I18nFeedback.config.show_pill ? true : false,
|
|
39
39
|
pillLabel: I18nFeedback.config.pill_label,
|
|
40
40
|
toggleParam: I18nFeedback.config.toggle_param,
|
|
41
|
-
labels: labels,
|
|
41
|
+
labels: labels(locale),
|
|
42
42
|
rtl: rtl?(locale)
|
|
43
43
|
}
|
|
44
44
|
# Escape "</" so a value can't close the <script> block early.
|
|
@@ -52,28 +52,36 @@ module I18nFeedback
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
54
|
# Every user-facing string in the widget, resolved through Rails I18n so the
|
|
55
|
-
# popover
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
55
|
+
# popover matches the locale the page was rendered in. The locale is passed
|
|
56
|
+
# explicitly (not read from I18n.locale) because auto-injection runs in
|
|
57
|
+
# middleware *after* the controller action, by which point an
|
|
58
|
+
# `around_action { I18n.with_locale(...) }` has reset the ambient locale
|
|
59
|
+
# back to the default. Each lookup carries an English default, so the widget
|
|
60
|
+
# stays fully worded even for a locale the gem ships no translation for.
|
|
61
|
+
def labels(locale)
|
|
59
62
|
{
|
|
60
|
-
pill: t(:pill, 'Suggest edits'),
|
|
61
|
-
pillActive: t(:pill_active, 'Suggesting — tap to exit (Esc)'),
|
|
62
|
-
title: t(:title, 'Suggest a translation fix'),
|
|
63
|
-
currentText: t(:current_text, 'Current text'),
|
|
64
|
-
suggestedText: t(:suggested_text, 'Suggested text'),
|
|
65
|
-
comment: t(:comment, 'Comment'),
|
|
66
|
-
commentPlaceholder: t(:comment_placeholder, 'Optional note for the developer'),
|
|
67
|
-
priorTitle: t(:prior_title, 'Already suggested (pending)'),
|
|
68
|
-
cancel: t(:cancel, 'Cancel'),
|
|
69
|
-
save: t(:save, 'Send suggestion'),
|
|
70
|
-
errorBlank: t(:error_blank, 'Please enter a suggestion.'),
|
|
71
|
-
errorSave: t(:error_save, 'Could not save the suggestion.')
|
|
63
|
+
pill: t(locale, :pill, 'Suggest edits'),
|
|
64
|
+
pillActive: t(locale, :pill_active, 'Suggesting — tap to exit (Esc)'),
|
|
65
|
+
title: t(locale, :title, 'Suggest a translation fix'),
|
|
66
|
+
currentText: t(locale, :current_text, 'Current text'),
|
|
67
|
+
suggestedText: t(locale, :suggested_text, 'Suggested text'),
|
|
68
|
+
comment: t(locale, :comment, 'Comment'),
|
|
69
|
+
commentPlaceholder: t(locale, :comment_placeholder, 'Optional note for the developer'),
|
|
70
|
+
priorTitle: t(locale, :prior_title, 'Already suggested (pending)'),
|
|
71
|
+
cancel: t(locale, :cancel, 'Cancel'),
|
|
72
|
+
save: t(locale, :save, 'Send suggestion'),
|
|
73
|
+
errorBlank: t(locale, :error_blank, 'Please enter a suggestion.'),
|
|
74
|
+
errorSave: t(locale, :error_save, 'Could not save the suggestion.')
|
|
72
75
|
}
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
# Resolve under the given locale, tolerating one the app doesn't list in
|
|
79
|
+
# available_locales (enforce_available_locales would otherwise raise) — the
|
|
80
|
+
# English default still applies.
|
|
81
|
+
def t(locale, key, default)
|
|
82
|
+
I18n.t(key, scope: :i18n_feedback, locale: locale, default: default)
|
|
83
|
+
rescue I18n::InvalidLocale
|
|
84
|
+
default
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def rtl?(locale)
|