i18n_feedback 0.2.1 → 0.3.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 +95 -6
- data/app/assets/i18n_feedback/widget.js +71 -19
- data/config/locales/i18n_feedback.ar.yml +14 -0
- data/config/locales/i18n_feedback.bn.yml +14 -0
- data/config/locales/i18n_feedback.de.yml +14 -0
- data/config/locales/i18n_feedback.en.yml +14 -0
- data/config/locales/i18n_feedback.es.yml +14 -0
- data/config/locales/i18n_feedback.fr.yml +14 -0
- data/config/locales/i18n_feedback.hi.yml +14 -0
- data/config/locales/i18n_feedback.id.yml +14 -0
- data/config/locales/i18n_feedback.it.yml +14 -0
- data/config/locales/i18n_feedback.ja.yml +14 -0
- data/config/locales/i18n_feedback.ko.yml +14 -0
- data/config/locales/i18n_feedback.nl.yml +14 -0
- data/config/locales/i18n_feedback.pl.yml +14 -0
- data/config/locales/i18n_feedback.pt.yml +14 -0
- data/config/locales/i18n_feedback.ru.yml +14 -0
- data/config/locales/i18n_feedback.th.yml +14 -0
- data/config/locales/i18n_feedback.tr.yml +14 -0
- data/config/locales/i18n_feedback.uk.yml +14 -0
- data/config/locales/i18n_feedback.ur.yml +14 -0
- data/config/locales/i18n_feedback.vi.yml +14 -0
- data/config/locales/i18n_feedback.zh-CN.yml +14 -0
- data/lib/i18n_feedback/configuration.rb +3 -2
- data/lib/i18n_feedback/middleware.rb +21 -7
- data/lib/i18n_feedback/version.rb +1 -1
- data/lib/i18n_feedback/widget.rb +52 -6
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c389e87f5f47b445f94f6ea242e803159bb34b2e0847d2ccc87883d05b3a666
|
|
4
|
+
data.tar.gz: 62ef3573c3d1a2070b151bbd77848e6f3c2db2c3db2abe532b6c32f0e8afb78f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67b4232a078ccb30b36de48123023151659301f6036bbac5069ea46db65dabc094cdfff3f3bd6267d63efd43f98b2aaed6c7912d384fdff1da8979f5788c738c
|
|
7
|
+
data.tar.gz: 8f575349159537f6ee689d6abea8397ad425bfca692822c1ae93286934fa36bc8edba846b6a65572c5caf74e0fa6ce5ee752d29ad5e0c7df800329992355cae3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
- Localize the widget's own UI. Every string in the pill and the suggestion
|
|
8
|
+
popover now resolves through Rails I18n under the `i18n_feedback.*` scope and
|
|
9
|
+
follows the app's current `I18n.locale`, so the tool speaks the same language as
|
|
10
|
+
the app being proofread. Any key a host hasn't translated falls back to English
|
|
11
|
+
— so nothing goes blank in a locale you haven't fully covered. Override any
|
|
12
|
+
string by defining the matching key in your own locale files.
|
|
13
|
+
- Ship translations out of the box for 20 languages in addition to English:
|
|
14
|
+
Arabic, Bengali, Chinese (Simplified), Dutch, French, German, Hindi, Indonesian,
|
|
15
|
+
Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish, Thai, Turkish,
|
|
16
|
+
Ukrainian, Urdu and Vietnamese.
|
|
17
|
+
- Render the popover right-to-left for RTL locales (Arabic, Urdu, and other RTL
|
|
18
|
+
scripts), detected from the active locale's language subtag. The i18n key stays
|
|
19
|
+
left-to-right, since it's a code identifier rather than prose.
|
|
20
|
+
- `config.pill_label` now defaults to `nil`, meaning "use the localized default".
|
|
21
|
+
Setting it to a string still overrides the pill text as before.
|
|
22
|
+
- The widget now follows the operating system's light/dark/system appearance via
|
|
23
|
+
`prefers-color-scheme`. The pill and popover render with a dark surface when the
|
|
24
|
+
reviewer's system is in dark mode, with no configuration required.
|
|
25
|
+
|
|
26
|
+
## [0.2.2]
|
|
27
|
+
|
|
28
|
+
- Fix suggest mode desyncing under a nonce-based CSP on Turbo visits. The runtime
|
|
29
|
+
config now rides in a `<script type="application/json">` block (data, not code)
|
|
30
|
+
that the widget re-reads on every `turbo:load`, instead of an executable
|
|
31
|
+
`<script>` the browser refuses to re-run when Turbo re-evaluates it with a stale
|
|
32
|
+
nonce. Only the widget code carries the CSP nonce now.
|
|
33
|
+
- Treat `?i18n_feedback=true|false` as a one-shot command: the middleware sets the
|
|
34
|
+
cookie and redirects (303) to the same URL without the parameter. The cookie is
|
|
35
|
+
now the single source of truth, so the parameter no longer sticks in the address
|
|
36
|
+
bar and the pill's reload can turn suggest mode off.
|
|
37
|
+
- Let a host's own toggle link work while suggest mode is active. Suggest mode
|
|
38
|
+
freezes navigation so a stray click can't leave the page mid-proofread, but that
|
|
39
|
+
also froze a `?i18n_feedback=false` link in your own nav — so the only way out
|
|
40
|
+
was the pill. Links carrying the toggle parameter are now exempt from the freeze.
|
|
41
|
+
|
|
5
42
|
## [0.2.1]
|
|
6
43
|
|
|
7
44
|
- Keep the suggest pill and active-mode highlighting working across Turbo Drive
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# i18n_feedback
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/i18n_feedback)
|
|
4
|
+
[](https://rubygems.org/gems/i18n_feedback)
|
|
5
|
+
[](https://github.com/yshmarov/i18n-feedback/actions/workflows/ci.yml)
|
|
6
|
+
[](MIT-LICENSE)
|
|
7
|
+
|
|
3
8
|
In-context translation proofreading for Rails.
|
|
4
9
|
|
|
5
10
|
`i18n_feedback` renders every translated string alongside its i18n key in the
|
|
@@ -7,6 +12,13 @@ environments you choose, lets a reviewer click any string in the running app and
|
|
|
7
12
|
suggest a better wording, and stores those suggestions for a developer to apply.
|
|
8
13
|
It is meant for development and staging, never production.
|
|
9
14
|
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
<video src="https://github.com/yshmarov/i18n-feedback/raw/main/i18n-feedback-demo-640-high.mp4" controls muted playsinline width="640">
|
|
18
|
+
Your browser can't play this video —
|
|
19
|
+
<a href="https://github.com/yshmarov/i18n-feedback/raw/main/i18n-feedback-demo-640-high.mp4">download it here</a>.
|
|
20
|
+
</video>
|
|
21
|
+
|
|
10
22
|
- **Zero UI dependencies.** The widget is plain JavaScript and styles itself. No
|
|
11
23
|
Tailwind, no daisyUI, no Stimulus, no importmap, no build step.
|
|
12
24
|
- **Zero layout changes.** The widget is injected into HTML responses
|
|
@@ -28,6 +40,13 @@ It is meant for development and staging, never production.
|
|
|
28
40
|
4. Suggestions are `POST`ed to the mounted engine and stored in the
|
|
29
41
|
`i18n_feedback_suggestions` table for you to review and apply.
|
|
30
42
|
|
|
43
|
+
## Turbo
|
|
44
|
+
|
|
45
|
+
Works with Turbo Drive out of the box. Turbo replaces `<body>` on every visit,
|
|
46
|
+
which would take the pill and the active-mode highlighting with it, so the
|
|
47
|
+
widget registers its document-level listeners once and re-renders on
|
|
48
|
+
`turbo:load`. The pill survives navigation without a full reload.
|
|
49
|
+
|
|
31
50
|
## Requirements
|
|
32
51
|
|
|
33
52
|
- Ruby >= 3.2
|
|
@@ -102,20 +121,41 @@ end
|
|
|
102
121
|
|
|
103
122
|
### Toggling suggest mode from your own link
|
|
104
123
|
|
|
105
|
-
Prefer a menu item over the floating pill?
|
|
106
|
-
|
|
124
|
+
Prefer a menu item over the floating pill? You can drive suggest mode from any
|
|
125
|
+
link in your own UI — a nav item, a sidebar entry, a footer — and optionally hide
|
|
126
|
+
the pill (the two can also coexist):
|
|
107
127
|
|
|
108
128
|
```ruby
|
|
109
|
-
config.show_pill = false
|
|
129
|
+
config.show_pill = false # optional
|
|
110
130
|
```
|
|
111
131
|
|
|
132
|
+
A one-way "turn it on" link is just the toggle parameter:
|
|
133
|
+
|
|
112
134
|
```erb
|
|
113
135
|
<%= link_to "Proofread translations", "?i18n_feedback=true" %>
|
|
114
136
|
```
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
For a single control that flips both ways, read the current state from the
|
|
139
|
+
`i18n_feedback` cookie and point at the opposite state:
|
|
140
|
+
|
|
141
|
+
```erb
|
|
142
|
+
<% on = cookies[:i18n_feedback].present? %>
|
|
143
|
+
<%= link_to (on ? "Disable translations editing" : "Enable translations editing"),
|
|
144
|
+
"?i18n_feedback=#{!on}" %>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Good to know:
|
|
148
|
+
|
|
149
|
+
- `?i18n_feedback=true` turns suggest mode on, `false` turns it off. The state is
|
|
150
|
+
stored in the `i18n_feedback` cookie, and the middleware then redirects to the
|
|
151
|
+
same URL without the parameter — so it never sticks in the address bar and the
|
|
152
|
+
cookie stays the single source of truth. `Esc` (or the pill) also exits.
|
|
153
|
+
- These links keep working **while suggest mode is active**. The widget freezes
|
|
154
|
+
ordinary navigation during proofreading (so a stray click can't leave the page
|
|
155
|
+
mid-edit), but any link carrying the toggle parameter is exempt — so a "Disable"
|
|
156
|
+
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.
|
|
119
159
|
|
|
120
160
|
### Gating examples
|
|
121
161
|
|
|
@@ -137,6 +177,49 @@ Set `config.auto_inject = false` and drop the helper at the end of your layout:
|
|
|
137
177
|
|
|
138
178
|
It renders nothing unless the tool is available for the request.
|
|
139
179
|
|
|
180
|
+
### Localizing the widget UI
|
|
181
|
+
|
|
182
|
+
The pill and the suggestion popover speak the app's language: every string
|
|
183
|
+
resolves through Rails I18n under the `i18n_feedback.*` scope and follows the
|
|
184
|
+
current `I18n.locale`. Translations ship out of the box for English plus 20 more
|
|
185
|
+
languages — Arabic, Bengali, Chinese (Simplified), Dutch, French, German, Hindi,
|
|
186
|
+
Indonesian, Italian, Japanese, Korean, Polish, Portuguese, Russian, Spanish,
|
|
187
|
+
Thai, Turkish, Ukrainian, Urdu and Vietnamese — so the tool is already localized
|
|
188
|
+
for most apps. RTL locales (Arabic, Urdu, …) render the popover right-to-left
|
|
189
|
+
automatically.
|
|
190
|
+
|
|
191
|
+
Any key you haven't translated falls back to English, so a partially translated
|
|
192
|
+
locale never leaves a control blank. To add a language, or reword the bundled
|
|
193
|
+
copy, define the keys in your own locale files (yours win over the gem's):
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
# config/locales/fr.yml
|
|
197
|
+
fr:
|
|
198
|
+
i18n_feedback:
|
|
199
|
+
pill: "Proposer des corrections"
|
|
200
|
+
pill_active: "En cours — appuyez pour quitter (Échap)"
|
|
201
|
+
title: "Proposer une correction de traduction"
|
|
202
|
+
current_text: "Texte actuel"
|
|
203
|
+
suggested_text: "Texte proposé"
|
|
204
|
+
comment: "Commentaire"
|
|
205
|
+
comment_placeholder: "Note facultative pour le développeur"
|
|
206
|
+
prior_title: "Déjà proposé (en attente)"
|
|
207
|
+
cancel: "Annuler"
|
|
208
|
+
save: "Envoyer la suggestion"
|
|
209
|
+
error_blank: "Veuillez saisir une suggestion."
|
|
210
|
+
error_save: "Impossible d'enregistrer la suggestion."
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`config.pill_label` still overrides the pill text with a fixed string if you set
|
|
214
|
+
it; leave it `nil` (the default) to use the localized `i18n_feedback.pill` key.
|
|
215
|
+
|
|
216
|
+
### Light / dark / system appearance
|
|
217
|
+
|
|
218
|
+
The widget follows the reviewer's operating-system appearance via
|
|
219
|
+
`prefers-color-scheme` — no configuration needed. The pill and popover render on a
|
|
220
|
+
dark surface when the system is in dark mode and a light surface otherwise; the
|
|
221
|
+
blue accent stays the same in both.
|
|
222
|
+
|
|
140
223
|
## Reviewing suggestions
|
|
141
224
|
|
|
142
225
|
Suggestions are ordinary records:
|
|
@@ -157,6 +240,12 @@ Each row stores `translation_key`, `locale`, `old_value`, `proposed_value`,
|
|
|
157
240
|
`config.enabled` returns true.
|
|
158
241
|
- Format and lookup namespaces (`number.*`, `date.*`, `*_html` formats, etc.) are
|
|
159
242
|
never marked, so currency and date formatting are unaffected.
|
|
243
|
+
- The injected widget code carries the request's Content-Security-Policy nonce
|
|
244
|
+
(the same one `ActionDispatch` emits), so it runs under a nonce-based
|
|
245
|
+
`script-src` policy — including `strict-dynamic` — with no configuration. It is
|
|
246
|
+
a no-op when the app sets no CSP nonce. The runtime config is shipped as a
|
|
247
|
+
`<script type="application/json">` block (data, not code), so it needs no nonce
|
|
248
|
+
and stays correct across Turbo visits.
|
|
160
249
|
|
|
161
250
|
## Development
|
|
162
251
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* i18n_feedback widget — self-contained, no framework, no build step.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
4
|
+
* Reads its config from the <script type="application/json"
|
|
5
|
+
* data-i18n-feedback-config> the server injects — re-read on every render so a
|
|
6
|
+
* Turbo visit always reflects the current page's suggest state.
|
|
5
7
|
*
|
|
6
8
|
* A floating pill toggles "suggest mode", which is server state: the pill sets or
|
|
7
9
|
* clears the i18n_feedback cookie and reloads, so the backend only prints the
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
(function () {
|
|
15
17
|
"use strict";
|
|
16
18
|
|
|
17
|
-
var config =
|
|
19
|
+
var config = readConfig();
|
|
18
20
|
if (!config || window.__i18nFeedbackLoaded) return;
|
|
19
21
|
window.__i18nFeedbackLoaded = true;
|
|
20
22
|
|
|
@@ -43,8 +45,10 @@
|
|
|
43
45
|
|
|
44
46
|
ready(function () {
|
|
45
47
|
// Document-level listeners survive Turbo navigations, so register them once.
|
|
48
|
+
// handleClick is always attached and gates on the *current* config.active, so
|
|
49
|
+
// toggling suggest mode across a Turbo visit needs no re-registration.
|
|
46
50
|
document.addEventListener("keydown", handleKeydown);
|
|
47
|
-
|
|
51
|
+
document.addEventListener("click", handleClick, true);
|
|
48
52
|
|
|
49
53
|
// Everything else lives in <body>, which Turbo replaces on every visit —
|
|
50
54
|
// taking the pill and the active-mode highlighting with it. Re-run the
|
|
@@ -55,13 +59,24 @@
|
|
|
55
59
|
document.addEventListener("turbo:frame-load", strip);
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
function readConfig() {
|
|
63
|
+
var el = document.querySelector("script[data-i18n-feedback-config]");
|
|
64
|
+
if (!el) return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(el.textContent);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
function render() {
|
|
73
|
+
// Re-read on each visit: the injected config is data (not an executed
|
|
74
|
+
// script), so it reflects the page Turbo just rendered.
|
|
75
|
+
config = readConfig() || config;
|
|
59
76
|
injectStyles();
|
|
60
77
|
if (config.showPill !== false) buildPill();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
strip();
|
|
64
|
-
}
|
|
78
|
+
document.documentElement.classList.toggle("i18nf-active", !!config.active);
|
|
79
|
+
if (config.active) strip();
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
// --- suggest-mode toggle --------------------------------------------------
|
|
@@ -112,8 +127,13 @@
|
|
|
112
127
|
// --- interaction ----------------------------------------------------------
|
|
113
128
|
|
|
114
129
|
function handleClick(event) {
|
|
130
|
+
if (!config.active) return;
|
|
115
131
|
if (overlay && overlay.contains(event.target)) return;
|
|
116
132
|
if (event.target.closest && event.target.closest(".i18nf-pill")) return;
|
|
133
|
+
// Let a host's own suggest-mode toggle link through (e.g. "?i18n_feedback=false"
|
|
134
|
+
// in a nav menu); otherwise navigation-freezing would trap the user in suggest
|
|
135
|
+
// mode with no way out but the pill.
|
|
136
|
+
if (event.target.closest && event.target.closest('a[href*="' + config.toggleParam + '="]')) return;
|
|
117
137
|
|
|
118
138
|
// Freeze navigation so a stray click can't leave the page mid-proofread.
|
|
119
139
|
event.preventDefault();
|
|
@@ -138,13 +158,19 @@
|
|
|
138
158
|
var pill = document.createElement("button");
|
|
139
159
|
pill.type = "button";
|
|
140
160
|
pill.className = "i18nf-pill" + (config.active ? " i18nf-pill-on" : "");
|
|
141
|
-
pill.textContent = config.active ? "✓ Suggesting — tap to exit (Esc)" : "✎ " + labelText();
|
|
161
|
+
pill.textContent = config.active ? "✓ " + t("pillActive", "Suggesting — tap to exit (Esc)") : "✎ " + labelText();
|
|
142
162
|
pill.addEventListener("click", toggle);
|
|
143
163
|
document.body.appendChild(pill);
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
function labelText() {
|
|
147
|
-
return
|
|
167
|
+
return config.pillLabel || t("pill", "Suggest edits");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Resolve a server-translated label, falling back to English if the config
|
|
171
|
+
// predates the labels payload (e.g. a cached page from an older gem version).
|
|
172
|
+
function t(name, fallback) {
|
|
173
|
+
return (config.labels && config.labels[name]) || fallback;
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
// --- popover --------------------------------------------------------------
|
|
@@ -158,18 +184,21 @@
|
|
|
158
184
|
});
|
|
159
185
|
|
|
160
186
|
var panel = el("div", "i18nf-panel");
|
|
187
|
+
// Mirror the popover for right-to-left locales (Arabic, Urdu, …). The i18n
|
|
188
|
+
// key stays LTR — it's a code identifier, not prose (see heading()).
|
|
189
|
+
panel.dir = config.rtl ? "rtl" : "ltr";
|
|
161
190
|
panel.appendChild(heading(key));
|
|
162
191
|
|
|
163
192
|
priorNode = el("div");
|
|
164
193
|
panel.appendChild(priorNode);
|
|
165
194
|
|
|
166
|
-
panel.appendChild(field("Current text", readonly(currentValue)));
|
|
195
|
+
panel.appendChild(field(t("currentText", "Current text"), readonly(currentValue)));
|
|
167
196
|
|
|
168
197
|
proposedInput = textarea(currentValue);
|
|
169
|
-
panel.appendChild(field("Suggested text", proposedInput));
|
|
198
|
+
panel.appendChild(field(t("suggestedText", "Suggested text"), proposedInput));
|
|
170
199
|
|
|
171
|
-
commentInput = input("Optional note for the developer");
|
|
172
|
-
panel.appendChild(field("Comment", commentInput));
|
|
200
|
+
commentInput = input(t("commentPlaceholder", "Optional note for the developer"));
|
|
201
|
+
panel.appendChild(field(t("comment", "Comment"), commentInput));
|
|
173
202
|
|
|
174
203
|
errorNode = el("p", "i18nf-error");
|
|
175
204
|
errorNode.style.display = "none";
|
|
@@ -209,7 +238,7 @@
|
|
|
209
238
|
|
|
210
239
|
var box = el("div", "i18nf-prior");
|
|
211
240
|
var title = el("p", "i18nf-prior-title");
|
|
212
|
-
title.textContent = "Already suggested (pending)";
|
|
241
|
+
title.textContent = t("priorTitle", "Already suggested (pending)");
|
|
213
242
|
box.appendChild(title);
|
|
214
243
|
|
|
215
244
|
var list = el("ul", "i18nf-prior-list");
|
|
@@ -230,12 +259,12 @@
|
|
|
230
259
|
|
|
231
260
|
var cancel = el("button", "i18nf-btn i18nf-btn-ghost");
|
|
232
261
|
cancel.type = "button";
|
|
233
|
-
cancel.textContent = "Cancel";
|
|
262
|
+
cancel.textContent = t("cancel", "Cancel");
|
|
234
263
|
cancel.addEventListener("click", close);
|
|
235
264
|
|
|
236
265
|
saveButton = el("button", "i18nf-btn i18nf-btn-primary");
|
|
237
266
|
saveButton.type = "button";
|
|
238
|
-
saveButton.textContent = "Send suggestion";
|
|
267
|
+
saveButton.textContent = t("save", "Send suggestion");
|
|
239
268
|
saveButton.addEventListener("click", function () {
|
|
240
269
|
submit(key, currentValue);
|
|
241
270
|
});
|
|
@@ -248,7 +277,7 @@
|
|
|
248
277
|
function submit(key, currentValue) {
|
|
249
278
|
var proposed = proposedInput.value.trim();
|
|
250
279
|
if (!proposed) {
|
|
251
|
-
showError("Please enter a suggestion.");
|
|
280
|
+
showError(t("errorBlank", "Please enter a suggestion."));
|
|
252
281
|
return;
|
|
253
282
|
}
|
|
254
283
|
|
|
@@ -277,7 +306,7 @@
|
|
|
277
306
|
close();
|
|
278
307
|
} else {
|
|
279
308
|
saveButton.disabled = false;
|
|
280
|
-
showError("Could not save the suggestion.");
|
|
309
|
+
showError(t("errorSave", "Could not save the suggestion."));
|
|
281
310
|
}
|
|
282
311
|
})
|
|
283
312
|
.catch(function () {
|
|
@@ -296,8 +325,9 @@
|
|
|
296
325
|
function heading(key) {
|
|
297
326
|
var wrapper = el("div", "i18nf-heading");
|
|
298
327
|
var title = el("p", "i18nf-title");
|
|
299
|
-
title.textContent = "Suggest a translation fix";
|
|
328
|
+
title.textContent = t("title", "Suggest a translation fix");
|
|
300
329
|
var code = el("code", "i18nf-key");
|
|
330
|
+
code.dir = "ltr"; // the key is a code path, never RTL prose
|
|
301
331
|
code.textContent = key;
|
|
302
332
|
wrapper.appendChild(title);
|
|
303
333
|
wrapper.appendChild(code);
|
|
@@ -385,6 +415,28 @@
|
|
|
385
415
|
".i18nf-btn-ghost { background: transparent; color: #111; }",
|
|
386
416
|
".i18nf-btn-primary { background: #2563eb; color: #fff; }",
|
|
387
417
|
".i18nf-btn[disabled] { opacity: .6; cursor: default; }",
|
|
418
|
+
|
|
419
|
+
// Follow the OS light/dark/system setting via prefers-color-scheme, so the
|
|
420
|
+
// widget matches whatever the reviewer's system is set to without any extra
|
|
421
|
+
// configuration. Only the surfaces that carry their own background/color
|
|
422
|
+
// above are overridden here — the blue accents stay the same in both themes.
|
|
423
|
+
"@media (prefers-color-scheme: dark) {",
|
|
424
|
+
" .i18nf-pill { background: #1f1f23; color: #f4f4f5; border-color: rgba(255,255,255,.18);",
|
|
425
|
+
" box-shadow: 0 4px 14px rgba(0,0,0,.5); }",
|
|
426
|
+
" .i18nf-pill-on { background: #2563eb; color: #fff; border-color: #2563eb; }",
|
|
427
|
+
" .i18nf-overlay { background: rgba(0,0,0,.65); }",
|
|
428
|
+
" .i18nf-panel { background: #1f1f23; color: #f4f4f5;",
|
|
429
|
+
" box-shadow: 0 20px 60px rgba(0,0,0,.7); }",
|
|
430
|
+
" .i18nf-key { color: #a1a1aa; }",
|
|
431
|
+
" .i18nf-label { color: #a1a1aa; }",
|
|
432
|
+
" .i18nf-readonly { background: #2a2a30; }",
|
|
433
|
+
" .i18nf-input { background: #2a2a30; color: #f4f4f5; border-color: #3f3f46; }",
|
|
434
|
+
" .i18nf-input::placeholder { color: #71717a; }",
|
|
435
|
+
" .i18nf-prior { background: #2a2a30; }",
|
|
436
|
+
" .i18nf-prior-title { color: #d4d4d8; }",
|
|
437
|
+
" .i18nf-error { color: #f87171; }",
|
|
438
|
+
" .i18nf-btn-ghost { color: #f4f4f5; }",
|
|
439
|
+
"}",
|
|
388
440
|
].join("\n");
|
|
389
441
|
document.head.appendChild(style);
|
|
390
442
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ar:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "اقترح تعديلات"
|
|
4
|
+
pill_active: "وضع الاقتراح — اضغط للخروج (Esc)"
|
|
5
|
+
title: "اقترح تصحيحًا للترجمة"
|
|
6
|
+
current_text: "النص الحالي"
|
|
7
|
+
suggested_text: "النص المقترح"
|
|
8
|
+
comment: "تعليق"
|
|
9
|
+
comment_placeholder: "ملاحظة اختيارية للمطور"
|
|
10
|
+
prior_title: "تم اقتراحه مسبقًا (قيد الانتظار)"
|
|
11
|
+
cancel: "إلغاء"
|
|
12
|
+
save: "إرسال الاقتراح"
|
|
13
|
+
error_blank: "يرجى إدخال اقتراح."
|
|
14
|
+
error_save: "تعذّر حفظ الاقتراح."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
bn:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "সম্পাদনা প্রস্তাব করুন"
|
|
4
|
+
pill_active: "প্রস্তাব চলছে — বের হতে ট্যাপ করুন (Esc)"
|
|
5
|
+
title: "অনুবাদ সংশোধন প্রস্তাব করুন"
|
|
6
|
+
current_text: "বর্তমান লেখা"
|
|
7
|
+
suggested_text: "প্রস্তাবিত লেখা"
|
|
8
|
+
comment: "মন্তব্য"
|
|
9
|
+
comment_placeholder: "ডেভেলপারের জন্য ঐচ্ছিক নোট"
|
|
10
|
+
prior_title: "ইতিমধ্যে প্রস্তাবিত (অপেক্ষমাণ)"
|
|
11
|
+
cancel: "বাতিল"
|
|
12
|
+
save: "প্রস্তাব পাঠান"
|
|
13
|
+
error_blank: "অনুগ্রহ করে একটি প্রস্তাব লিখুন।"
|
|
14
|
+
error_save: "প্রস্তাব সংরক্ষণ করা যায়নি।"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
de:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Änderungen vorschlagen"
|
|
4
|
+
pill_active: "Vorschlagsmodus — zum Beenden tippen (Esc)"
|
|
5
|
+
title: "Übersetzungskorrektur vorschlagen"
|
|
6
|
+
current_text: "Aktueller Text"
|
|
7
|
+
suggested_text: "Vorgeschlagener Text"
|
|
8
|
+
comment: "Kommentar"
|
|
9
|
+
comment_placeholder: "Optionale Notiz für die Entwicklung"
|
|
10
|
+
prior_title: "Bereits vorgeschlagen (ausstehend)"
|
|
11
|
+
cancel: "Abbrechen"
|
|
12
|
+
save: "Vorschlag senden"
|
|
13
|
+
error_blank: "Bitte gib einen Vorschlag ein."
|
|
14
|
+
error_save: "Der Vorschlag konnte nicht gespeichert werden."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
en:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Suggest edits"
|
|
4
|
+
pill_active: "Suggesting — tap to exit (Esc)"
|
|
5
|
+
title: "Suggest a translation fix"
|
|
6
|
+
current_text: "Current text"
|
|
7
|
+
suggested_text: "Suggested text"
|
|
8
|
+
comment: "Comment"
|
|
9
|
+
comment_placeholder: "Optional note for the developer"
|
|
10
|
+
prior_title: "Already suggested (pending)"
|
|
11
|
+
cancel: "Cancel"
|
|
12
|
+
save: "Send suggestion"
|
|
13
|
+
error_blank: "Please enter a suggestion."
|
|
14
|
+
error_save: "Could not save the suggestion."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
es:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Sugerir cambios"
|
|
4
|
+
pill_active: "Sugiriendo — toca para salir (Esc)"
|
|
5
|
+
title: "Sugerir una corrección de traducción"
|
|
6
|
+
current_text: "Texto actual"
|
|
7
|
+
suggested_text: "Texto sugerido"
|
|
8
|
+
comment: "Comentario"
|
|
9
|
+
comment_placeholder: "Nota opcional para el desarrollador"
|
|
10
|
+
prior_title: "Ya sugerido (pendiente)"
|
|
11
|
+
cancel: "Cancelar"
|
|
12
|
+
save: "Enviar sugerencia"
|
|
13
|
+
error_blank: "Introduce una sugerencia."
|
|
14
|
+
error_save: "No se pudo guardar la sugerencia."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Proposer des corrections"
|
|
4
|
+
pill_active: "En cours — appuyez pour quitter (Échap)"
|
|
5
|
+
title: "Proposer une correction de traduction"
|
|
6
|
+
current_text: "Texte actuel"
|
|
7
|
+
suggested_text: "Texte proposé"
|
|
8
|
+
comment: "Commentaire"
|
|
9
|
+
comment_placeholder: "Note facultative pour le développeur"
|
|
10
|
+
prior_title: "Déjà proposé (en attente)"
|
|
11
|
+
cancel: "Annuler"
|
|
12
|
+
save: "Envoyer la suggestion"
|
|
13
|
+
error_blank: "Veuillez saisir une suggestion."
|
|
14
|
+
error_save: "Impossible d'enregistrer la suggestion."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
hi:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "बदलाव सुझाएँ"
|
|
4
|
+
pill_active: "सुझाव मोड — बाहर निकलने के लिए टैप करें (Esc)"
|
|
5
|
+
title: "अनुवाद सुधार सुझाएँ"
|
|
6
|
+
current_text: "वर्तमान टेक्स्ट"
|
|
7
|
+
suggested_text: "सुझाया गया टेक्स्ट"
|
|
8
|
+
comment: "टिप्पणी"
|
|
9
|
+
comment_placeholder: "डेवलपर के लिए वैकल्पिक टिप्पणी"
|
|
10
|
+
prior_title: "पहले से सुझाया गया (लंबित)"
|
|
11
|
+
cancel: "रद्द करें"
|
|
12
|
+
save: "सुझाव भेजें"
|
|
13
|
+
error_blank: "कृपया एक सुझाव दर्ज करें।"
|
|
14
|
+
error_save: "सुझाव सहेजा नहीं जा सका।"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
id:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Sarankan perubahan"
|
|
4
|
+
pill_active: "Menyarankan — ketuk untuk keluar (Esc)"
|
|
5
|
+
title: "Sarankan perbaikan terjemahan"
|
|
6
|
+
current_text: "Teks saat ini"
|
|
7
|
+
suggested_text: "Teks yang disarankan"
|
|
8
|
+
comment: "Komentar"
|
|
9
|
+
comment_placeholder: "Catatan opsional untuk pengembang"
|
|
10
|
+
prior_title: "Sudah disarankan (menunggu)"
|
|
11
|
+
cancel: "Batal"
|
|
12
|
+
save: "Kirim saran"
|
|
13
|
+
error_blank: "Silakan masukkan saran."
|
|
14
|
+
error_save: "Tidak dapat menyimpan saran."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
it:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Suggerisci modifiche"
|
|
4
|
+
pill_active: "Suggerimento — tocca per uscire (Esc)"
|
|
5
|
+
title: "Suggerisci una correzione di traduzione"
|
|
6
|
+
current_text: "Testo attuale"
|
|
7
|
+
suggested_text: "Testo suggerito"
|
|
8
|
+
comment: "Commento"
|
|
9
|
+
comment_placeholder: "Nota facoltativa per lo sviluppatore"
|
|
10
|
+
prior_title: "Già suggerito (in sospeso)"
|
|
11
|
+
cancel: "Annulla"
|
|
12
|
+
save: "Invia suggerimento"
|
|
13
|
+
error_blank: "Inserisci un suggerimento."
|
|
14
|
+
error_save: "Impossibile salvare il suggerimento."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ja:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "修正を提案"
|
|
4
|
+
pill_active: "提案中 — タップで終了(Esc)"
|
|
5
|
+
title: "翻訳の修正を提案"
|
|
6
|
+
current_text: "現在のテキスト"
|
|
7
|
+
suggested_text: "提案するテキスト"
|
|
8
|
+
comment: "コメント"
|
|
9
|
+
comment_placeholder: "開発者への任意のメモ"
|
|
10
|
+
prior_title: "提案済み(保留中)"
|
|
11
|
+
cancel: "キャンセル"
|
|
12
|
+
save: "提案を送信"
|
|
13
|
+
error_blank: "提案を入力してください。"
|
|
14
|
+
error_save: "提案を保存できませんでした。"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ko:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "수정 제안"
|
|
4
|
+
pill_active: "제안 중 — 종료하려면 탭하세요 (Esc)"
|
|
5
|
+
title: "번역 수정 제안"
|
|
6
|
+
current_text: "현재 텍스트"
|
|
7
|
+
suggested_text: "제안 텍스트"
|
|
8
|
+
comment: "의견"
|
|
9
|
+
comment_placeholder: "개발자를 위한 선택 메모"
|
|
10
|
+
prior_title: "이미 제안됨 (대기 중)"
|
|
11
|
+
cancel: "취소"
|
|
12
|
+
save: "제안 보내기"
|
|
13
|
+
error_blank: "제안을 입력하세요."
|
|
14
|
+
error_save: "제안을 저장할 수 없습니다."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
nl:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Wijzigingen voorstellen"
|
|
4
|
+
pill_active: "Aan het voorstellen — tik om te sluiten (Esc)"
|
|
5
|
+
title: "Een vertaalcorrectie voorstellen"
|
|
6
|
+
current_text: "Huidige tekst"
|
|
7
|
+
suggested_text: "Voorgestelde tekst"
|
|
8
|
+
comment: "Opmerking"
|
|
9
|
+
comment_placeholder: "Optionele notitie voor de ontwikkelaar"
|
|
10
|
+
prior_title: "Al voorgesteld (in behandeling)"
|
|
11
|
+
cancel: "Annuleren"
|
|
12
|
+
save: "Suggestie versturen"
|
|
13
|
+
error_blank: "Voer een suggestie in."
|
|
14
|
+
error_save: "Kan de suggestie niet opslaan."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pl:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Zaproponuj zmiany"
|
|
4
|
+
pill_active: "Tryb sugestii — dotknij, aby wyjść (Esc)"
|
|
5
|
+
title: "Zaproponuj poprawkę tłumaczenia"
|
|
6
|
+
current_text: "Bieżący tekst"
|
|
7
|
+
suggested_text: "Proponowany tekst"
|
|
8
|
+
comment: "Komentarz"
|
|
9
|
+
comment_placeholder: "Opcjonalna notatka dla programisty"
|
|
10
|
+
prior_title: "Już zaproponowano (oczekujące)"
|
|
11
|
+
cancel: "Anuluj"
|
|
12
|
+
save: "Wyślij sugestię"
|
|
13
|
+
error_blank: "Wprowadź sugestię."
|
|
14
|
+
error_save: "Nie udało się zapisać sugestii."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pt:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Sugerir edições"
|
|
4
|
+
pill_active: "Sugerindo — toque para sair (Esc)"
|
|
5
|
+
title: "Sugerir uma correção de tradução"
|
|
6
|
+
current_text: "Texto atual"
|
|
7
|
+
suggested_text: "Texto sugerido"
|
|
8
|
+
comment: "Comentário"
|
|
9
|
+
comment_placeholder: "Nota opcional para o desenvolvedor"
|
|
10
|
+
prior_title: "Já sugerido (pendente)"
|
|
11
|
+
cancel: "Cancelar"
|
|
12
|
+
save: "Enviar sugestão"
|
|
13
|
+
error_blank: "Insira uma sugestão."
|
|
14
|
+
error_save: "Não foi possível salvar a sugestão."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ru:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Предложить правки"
|
|
4
|
+
pill_active: "Режим предложений — нажмите, чтобы выйти (Esc)"
|
|
5
|
+
title: "Предложить исправление перевода"
|
|
6
|
+
current_text: "Текущий текст"
|
|
7
|
+
suggested_text: "Предлагаемый текст"
|
|
8
|
+
comment: "Комментарий"
|
|
9
|
+
comment_placeholder: "Необязательная заметка для разработчика"
|
|
10
|
+
prior_title: "Уже предложено (в ожидании)"
|
|
11
|
+
cancel: "Отмена"
|
|
12
|
+
save: "Отправить предложение"
|
|
13
|
+
error_blank: "Введите предложение."
|
|
14
|
+
error_save: "Не удалось сохранить предложение."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
th:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "แนะนำการแก้ไข"
|
|
4
|
+
pill_active: "กำลังแนะนำ — แตะเพื่อออก (Esc)"
|
|
5
|
+
title: "แนะนำการแก้ไขคำแปล"
|
|
6
|
+
current_text: "ข้อความปัจจุบัน"
|
|
7
|
+
suggested_text: "ข้อความที่แนะนำ"
|
|
8
|
+
comment: "ความคิดเห็น"
|
|
9
|
+
comment_placeholder: "หมายเหตุเพิ่มเติมสำหรับนักพัฒนา"
|
|
10
|
+
prior_title: "แนะนำไปแล้ว (รอดำเนินการ)"
|
|
11
|
+
cancel: "ยกเลิก"
|
|
12
|
+
save: "ส่งคำแนะนำ"
|
|
13
|
+
error_blank: "โปรดป้อนคำแนะนำ"
|
|
14
|
+
error_save: "ไม่สามารถบันทึกคำแนะนำได้"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
tr:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Düzenleme öner"
|
|
4
|
+
pill_active: "Öneri modu — çıkmak için dokun (Esc)"
|
|
5
|
+
title: "Çeviri düzeltmesi öner"
|
|
6
|
+
current_text: "Mevcut metin"
|
|
7
|
+
suggested_text: "Önerilen metin"
|
|
8
|
+
comment: "Yorum"
|
|
9
|
+
comment_placeholder: "Geliştirici için isteğe bağlı not"
|
|
10
|
+
prior_title: "Zaten önerildi (beklemede)"
|
|
11
|
+
cancel: "İptal"
|
|
12
|
+
save: "Öneriyi gönder"
|
|
13
|
+
error_blank: "Lütfen bir öneri girin."
|
|
14
|
+
error_save: "Öneri kaydedilemedi."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
uk:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Запропонувати зміни"
|
|
4
|
+
pill_active: "Режим пропозицій — торкніться, щоб вийти (Esc)"
|
|
5
|
+
title: "Запропонувати виправлення перекладу"
|
|
6
|
+
current_text: "Поточний текст"
|
|
7
|
+
suggested_text: "Запропонований текст"
|
|
8
|
+
comment: "Коментар"
|
|
9
|
+
comment_placeholder: "Необов’язкова примітка для розробника"
|
|
10
|
+
prior_title: "Уже запропоновано (очікує)"
|
|
11
|
+
cancel: "Скасувати"
|
|
12
|
+
save: "Надіслати пропозицію"
|
|
13
|
+
error_blank: "Будь ласка, введіть пропозицію."
|
|
14
|
+
error_save: "Не вдалося зберегти пропозицію."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ur:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "ترامیم تجویز کریں"
|
|
4
|
+
pill_active: "تجویز موڈ — باہر نکلنے کے لیے تھپتھپائیں (Esc)"
|
|
5
|
+
title: "ترجمے کی اصلاح تجویز کریں"
|
|
6
|
+
current_text: "موجودہ متن"
|
|
7
|
+
suggested_text: "تجویز کردہ متن"
|
|
8
|
+
comment: "تبصرہ"
|
|
9
|
+
comment_placeholder: "ڈویلپر کے لیے اختیاری نوٹ"
|
|
10
|
+
prior_title: "پہلے ہی تجویز کیا جا چکا (زیر التوا)"
|
|
11
|
+
cancel: "منسوخ کریں"
|
|
12
|
+
save: "تجویز بھیجیں"
|
|
13
|
+
error_blank: "براہ کرم ایک تجویز درج کریں۔"
|
|
14
|
+
error_save: "تجویز محفوظ نہیں ہو سکی۔"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
vi:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "Đề xuất chỉnh sửa"
|
|
4
|
+
pill_active: "Đang đề xuất — chạm để thoát (Esc)"
|
|
5
|
+
title: "Đề xuất sửa bản dịch"
|
|
6
|
+
current_text: "Văn bản hiện tại"
|
|
7
|
+
suggested_text: "Văn bản đề xuất"
|
|
8
|
+
comment: "Nhận xét"
|
|
9
|
+
comment_placeholder: "Ghi chú tùy chọn cho nhà phát triển"
|
|
10
|
+
prior_title: "Đã đề xuất (đang chờ)"
|
|
11
|
+
cancel: "Hủy"
|
|
12
|
+
save: "Gửi đề xuất"
|
|
13
|
+
error_blank: "Vui lòng nhập một đề xuất."
|
|
14
|
+
error_save: "Không thể lưu đề xuất."
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
zh-CN:
|
|
2
|
+
i18n_feedback:
|
|
3
|
+
pill: "建议修改"
|
|
4
|
+
pill_active: "建议中 — 点击退出(Esc)"
|
|
5
|
+
title: "建议翻译修正"
|
|
6
|
+
current_text: "当前文本"
|
|
7
|
+
suggested_text: "建议文本"
|
|
8
|
+
comment: "备注"
|
|
9
|
+
comment_placeholder: "给开发者的可选备注"
|
|
10
|
+
prior_title: "已提交建议(待处理)"
|
|
11
|
+
cancel: "取消"
|
|
12
|
+
save: "发送建议"
|
|
13
|
+
error_blank: "请输入建议。"
|
|
14
|
+
error_save: "无法保存建议。"
|
|
@@ -38,7 +38,8 @@ module I18nFeedback
|
|
|
38
38
|
# from your own link instead (see #toggle_param).
|
|
39
39
|
attr_accessor :show_pill
|
|
40
40
|
|
|
41
|
-
# Text on the floating toggle pill.
|
|
41
|
+
# Text on the floating toggle pill. Leave nil to use the localized default
|
|
42
|
+
# ("Suggest edits", translated via the `i18n_feedback.pill` I18n key).
|
|
42
43
|
attr_accessor :pill_label
|
|
43
44
|
|
|
44
45
|
# The query-string parameter that turns suggest mode on/off, so a host can
|
|
@@ -56,7 +57,7 @@ module I18nFeedback
|
|
|
56
57
|
@auto_inject = true
|
|
57
58
|
@mount_path = '/i18n_feedback'
|
|
58
59
|
@show_pill = true
|
|
59
|
-
@pill_label =
|
|
60
|
+
@pill_label = nil
|
|
60
61
|
@toggle_param = 'i18n_feedback'
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -18,17 +18,19 @@ module I18nFeedback
|
|
|
18
18
|
request = Rack::Request.new(env)
|
|
19
19
|
available = I18nFeedback.available?(request)
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
# "?i18n_feedback=true|false" is a one-shot command, not a sticky URL state:
|
|
22
|
+
# set the cookie and redirect to the same URL without the parameter. That
|
|
23
|
+
# keeps the cookie the single source of truth (so the pill's reload can turn
|
|
24
|
+
# suggest mode off) and stops the parameter from lingering in the address bar.
|
|
25
|
+
if available && request.get? && !(desired = toggle_override(request)).nil?
|
|
26
|
+
return toggle_redirect(request, desired)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
marking = available && cookie_on?(request)
|
|
26
30
|
Marking.enabled = marking
|
|
27
31
|
|
|
28
32
|
status, headers, body = @app.call(env)
|
|
29
33
|
|
|
30
|
-
headers = persist_choice(headers, override) if available && !override.nil?
|
|
31
|
-
|
|
32
34
|
if available && I18nFeedback.config.auto_inject && html?(headers) && !request.xhr?
|
|
33
35
|
status, headers, body = inject(status, headers, body, marking, csp_nonce(env))
|
|
34
36
|
end
|
|
@@ -44,6 +46,18 @@ module I18nFeedback
|
|
|
44
46
|
!request.cookies[COOKIE].to_s.empty?
|
|
45
47
|
end
|
|
46
48
|
|
|
49
|
+
def toggle_redirect(request, desired)
|
|
50
|
+
headers = persist_choice({ 'Content-Type' => 'text/html', 'Location' => url_without_toggle(request) }, desired)
|
|
51
|
+
[303, headers, []]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def url_without_toggle(request)
|
|
55
|
+
query = Rack::Utils.parse_query(request.query_string)
|
|
56
|
+
query.delete(I18nFeedback.config.toggle_param)
|
|
57
|
+
built = Rack::Utils.build_query(query)
|
|
58
|
+
built.empty? ? request.path : "#{request.path}?#{built}"
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
# The request's CSP nonce, so the injected scripts run under a nonce-based
|
|
48
62
|
# Content-Security-Policy. Reads the value Rails memoizes on the env, which is
|
|
49
63
|
# the same nonce the CSP header uses. nil when the app sets no nonce.
|
data/lib/i18n_feedback/widget.rb
CHANGED
|
@@ -11,28 +11,74 @@ module I18nFeedback
|
|
|
11
11
|
module Widget
|
|
12
12
|
SOURCE = File.expand_path('../../app/assets/i18n_feedback/widget.js', __dir__)
|
|
13
13
|
|
|
14
|
+
# Right-to-left scripts, so the popover renders mirrored for those locales.
|
|
15
|
+
# Matched on the language subtag, so region variants ("ar-EG") count too.
|
|
16
|
+
RTL_LANGUAGES = %w[ar arc ckb dv fa ha he ks ku ps sd ug ur yi].freeze
|
|
17
|
+
|
|
14
18
|
class << self
|
|
15
19
|
def javascript
|
|
16
20
|
@javascript ||= File.read(SOURCE)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
|
-
# The two <script> tags to place before </body
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
+
# The two <script> tags to place before </body>.
|
|
24
|
+
#
|
|
25
|
+
# The config rides in a `type="application/json"` block: it is *data*, not
|
|
26
|
+
# code, so the browser never executes it and Turbo never tries to re-run it
|
|
27
|
+
# on a soft visit — which means it needs no CSP nonce and, crucially, the
|
|
28
|
+
# widget can re-read the *current* page's config on every `turbo:load`
|
|
29
|
+
# instead of being stuck with whatever the last full load evaluated.
|
|
30
|
+
#
|
|
31
|
+
# `nonce:` stamps only the widget script (the code), so it runs under a
|
|
32
|
+
# nonce-based Content-Security-Policy; pass nil when the app has no nonce.
|
|
23
33
|
def snippet(endpoint:, locale:, active:, nonce: nil)
|
|
24
34
|
config = {
|
|
25
35
|
endpoint: endpoint,
|
|
26
36
|
locale: locale.to_s,
|
|
27
37
|
active: active ? true : false,
|
|
28
38
|
showPill: I18nFeedback.config.show_pill ? true : false,
|
|
29
|
-
pillLabel: I18nFeedback.config.pill_label
|
|
39
|
+
pillLabel: I18nFeedback.config.pill_label,
|
|
40
|
+
toggleParam: I18nFeedback.config.toggle_param,
|
|
41
|
+
labels: labels,
|
|
42
|
+
rtl: rtl?(locale)
|
|
30
43
|
}
|
|
44
|
+
# Escape "</" so a value can't close the <script> block early.
|
|
45
|
+
json = config.to_json.gsub('</', '<\/')
|
|
31
46
|
nonce_attr = nonce ? %( nonce="#{nonce}") : ''
|
|
32
47
|
|
|
33
|
-
%(<script data-i18n-feedback
|
|
48
|
+
%(<script type="application/json" data-i18n-feedback-config>#{json}</script>) +
|
|
34
49
|
%(<script data-i18n-feedback-widget#{nonce_attr}>#{javascript}</script>)
|
|
35
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Every user-facing string in the widget, resolved through Rails I18n so the
|
|
55
|
+
# popover follows the app's current locale. Each lookup carries an English
|
|
56
|
+
# default, so the widget stays fully worded even when the host app hasn't
|
|
57
|
+
# loaded the gem's locale file or is missing a key for the active locale.
|
|
58
|
+
def labels
|
|
59
|
+
{
|
|
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.')
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def t(key, default)
|
|
76
|
+
I18n.t(key, scope: :i18n_feedback, default: default)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def rtl?(locale)
|
|
80
|
+
RTL_LANGUAGES.include?(locale.to_s.downcase.split(/[-_]/).first)
|
|
81
|
+
end
|
|
36
82
|
end
|
|
37
83
|
end
|
|
38
84
|
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
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -44,6 +44,27 @@ files:
|
|
|
44
44
|
- app/helpers/i18n_feedback/tag_helper.rb
|
|
45
45
|
- app/models/i18n_feedback/application_record.rb
|
|
46
46
|
- app/models/i18n_feedback/suggestion.rb
|
|
47
|
+
- config/locales/i18n_feedback.ar.yml
|
|
48
|
+
- config/locales/i18n_feedback.bn.yml
|
|
49
|
+
- config/locales/i18n_feedback.de.yml
|
|
50
|
+
- config/locales/i18n_feedback.en.yml
|
|
51
|
+
- config/locales/i18n_feedback.es.yml
|
|
52
|
+
- config/locales/i18n_feedback.fr.yml
|
|
53
|
+
- config/locales/i18n_feedback.hi.yml
|
|
54
|
+
- config/locales/i18n_feedback.id.yml
|
|
55
|
+
- config/locales/i18n_feedback.it.yml
|
|
56
|
+
- config/locales/i18n_feedback.ja.yml
|
|
57
|
+
- config/locales/i18n_feedback.ko.yml
|
|
58
|
+
- config/locales/i18n_feedback.nl.yml
|
|
59
|
+
- config/locales/i18n_feedback.pl.yml
|
|
60
|
+
- config/locales/i18n_feedback.pt.yml
|
|
61
|
+
- config/locales/i18n_feedback.ru.yml
|
|
62
|
+
- config/locales/i18n_feedback.th.yml
|
|
63
|
+
- config/locales/i18n_feedback.tr.yml
|
|
64
|
+
- config/locales/i18n_feedback.uk.yml
|
|
65
|
+
- config/locales/i18n_feedback.ur.yml
|
|
66
|
+
- config/locales/i18n_feedback.vi.yml
|
|
67
|
+
- config/locales/i18n_feedback.zh-CN.yml
|
|
47
68
|
- config/routes.rb
|
|
48
69
|
- lib/generators/i18n_feedback/install/install_generator.rb
|
|
49
70
|
- lib/generators/i18n_feedback/install/templates/create_i18n_feedback_suggestions.rb.tt
|