i18n_proofreading 0.10.2 → 0.10.3
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 +10 -0
- data/app/controllers/i18n_proofreading/widgets_controller.rb +14 -4
- data/app/views/layouts/i18n_proofreading/application.html.erb +3 -82
- data/config/routes.rb +1 -0
- data/lib/generators/i18n_proofreading/install/install_generator.rb +1 -1
- data/lib/generators/i18n_proofreading/install/templates/initializer.rb +2 -1
- data/lib/i18n_proofreading/dashboard.css +80 -0
- data/lib/i18n_proofreading/engine.rb +9 -0
- data/lib/i18n_proofreading/version.rb +1 -1
- data/lib/i18n_proofreading/widget.rb +9 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c234f2ba39e1d73f3ed201be05d4cce85a6550977b956943158fe353c9f13b50
|
|
4
|
+
data.tar.gz: 72a56aba394043a0300cb1500c877b9dd0b2eb6e33de2085b83948cdc5a1d9a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c520423553945e1c7e00699a143e37c1223658c11dfb9403c2998d67d1e0ca4ff7a6f9f874157aa2c2829ba62f681c89a11920ae0ef827dac20a160e0e32a71
|
|
7
|
+
data.tar.gz: 007dceabf146ee93b69a2e3940a16b6d47beb68fab0db80b5a51fc35a325e776ab4544c6e96b104fe4158e7b67845c296776a027e7a42935eafa6d01973951b3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.10.3]
|
|
6
|
+
|
|
7
|
+
- Added `mount_i18n_proofreading at: "/i18n_proofreading"` as the install-time
|
|
8
|
+
route helper, keeping `config.mount_path` synchronized with the mounted
|
|
9
|
+
engine path while preserving manual `mount I18nProofreading::Engine`
|
|
10
|
+
compatibility.
|
|
11
|
+
- Extracted the dashboard stylesheet into a same-origin, fingerprinted
|
|
12
|
+
`/dashboard.css` endpoint and added CSP meta tags to the dashboard layout.
|
|
13
|
+
The public widget remains pipeline-free and controller-served.
|
|
14
|
+
|
|
5
15
|
## [0.10.2]
|
|
6
16
|
|
|
7
17
|
- Added the `I18nProofreading` dashboard title to every shipped locale, matching
|
|
@@ -7,7 +7,7 @@ module I18nProofreading
|
|
|
7
7
|
# Drive swaps the <body> and re-runs body scripts under the *original*
|
|
8
8
|
# page's CSP header, where a freshly minted inline nonce would be refused.
|
|
9
9
|
class WidgetsController < ApplicationController
|
|
10
|
-
#
|
|
10
|
+
# These assets are static and carry no user data; without this, Rails'
|
|
11
11
|
# cross-origin JavaScript guard refuses to serve it to a plain
|
|
12
12
|
# <script src> request.
|
|
13
13
|
skip_forgery_protection
|
|
@@ -17,10 +17,20 @@ module I18nProofreading
|
|
|
17
17
|
# fingerprinted URL is immutable and gets long-lived caching; anything
|
|
18
18
|
# else only ETag-revalidates.
|
|
19
19
|
def show
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
serve(Widget.javascript, Widget.fingerprint, 'text/javascript')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dashboard_stylesheet
|
|
24
|
+
serve(Widget.dashboard_stylesheet, Widget.dashboard_stylesheet_fingerprint, 'text/css')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def serve(source, fingerprint, content_type)
|
|
30
|
+
expires_in 1.year, public: true if params[:v] == fingerprint
|
|
31
|
+
return unless stale?(etag: [I18nProofreading::VERSION, fingerprint])
|
|
22
32
|
|
|
23
|
-
render plain:
|
|
33
|
+
render plain: source, content_type: content_type
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -5,88 +5,9 @@
|
|
|
5
5
|
<title><%= t('i18n_proofreading.dashboard.title', default: 'I18nProofreading') %></title>
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
7
|
<%= csrf_meta_tags %>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
12
|
-
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff; --code: #f0f1f3;
|
|
13
|
-
--pending: #d97706; --applied: #16a34a; --rejected: #6b7280;
|
|
14
|
-
}
|
|
15
|
-
@media (prefers-color-scheme: dark) {
|
|
16
|
-
:root {
|
|
17
|
-
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
18
|
-
--border: #2a313a; --accent: #3b82f6; --code: #232a33;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
* { box-sizing: border-box; }
|
|
22
|
-
body {
|
|
23
|
-
margin: 0; background: var(--bg); color: var(--text);
|
|
24
|
-
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
25
|
-
}
|
|
26
|
-
a { color: var(--accent); text-decoration: none; }
|
|
27
|
-
a:hover { text-decoration: underline; }
|
|
28
|
-
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
29
|
-
h1 { font-size: 22px; margin: 0; }
|
|
30
|
-
.lede { color: var(--muted); margin: 4px 0 0; }
|
|
31
|
-
header.page { margin-bottom: 20px; }
|
|
32
|
-
|
|
33
|
-
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
34
|
-
.tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
|
|
35
|
-
.tabs a.active {
|
|
36
|
-
color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
37
|
-
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px;
|
|
38
|
-
}
|
|
39
|
-
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
40
|
-
.count {
|
|
41
|
-
display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
42
|
-
background: var(--border); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
46
|
-
.filters label { color: var(--muted); font-size: 13px; }
|
|
47
|
-
.filters select {
|
|
48
|
-
padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
|
|
49
|
-
background: var(--surface); color: var(--text); font: inherit;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.card {
|
|
53
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
54
|
-
padding: 14px 16px; margin-bottom: 12px;
|
|
55
|
-
}
|
|
56
|
-
.row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
57
|
-
code.key {
|
|
58
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
|
|
59
|
-
background: var(--code); padding: 2px 7px; border-radius: 6px; overflow-wrap: anywhere;
|
|
60
|
-
}
|
|
61
|
-
.badge {
|
|
62
|
-
display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
63
|
-
color: #fff; white-space: nowrap;
|
|
64
|
-
}
|
|
65
|
-
.badge.status-pending { background: var(--pending); }
|
|
66
|
-
.badge.status-applied { background: var(--applied); }
|
|
67
|
-
.badge.status-rejected { background: var(--rejected); }
|
|
68
|
-
.badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
69
|
-
.spacer { flex: 1; }
|
|
70
|
-
.meta { color: var(--muted); font-size: 13px; }
|
|
71
|
-
|
|
72
|
-
dl.diff { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
73
|
-
dl.diff dt { color: var(--muted); font-size: 13px; }
|
|
74
|
-
dl.diff dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
75
|
-
dd .old { color: var(--muted); }
|
|
76
|
-
dd .new { font-weight: 600; }
|
|
77
|
-
.comment { margin-top: 10px; padding: 8px 10px; background: var(--code); border-radius: 8px; font-size: 14px; overflow-wrap: anywhere; }
|
|
78
|
-
|
|
79
|
-
button {
|
|
80
|
-
padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
81
|
-
background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
|
|
82
|
-
}
|
|
83
|
-
button:hover { border-color: var(--muted); }
|
|
84
|
-
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
85
|
-
|
|
86
|
-
.empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
|
|
87
|
-
.pager { margin-top: 16px; display: flex; justify-content: space-between; }
|
|
88
|
-
.pager .muted { color: var(--muted); }
|
|
89
|
-
</style>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{I18nProofreading::Widget.dashboard_stylesheet_fingerprint}",
|
|
10
|
+
'data-turbo-track': 'reload' %>
|
|
90
11
|
</head>
|
|
91
12
|
<body>
|
|
92
13
|
<div class="container">
|
data/config/routes.rb
CHANGED
|
@@ -4,6 +4,7 @@ I18nProofreading::Engine.routes.draw do
|
|
|
4
4
|
# The widget code, served same-origin so it runs under a `'self'`/nonce-based
|
|
5
5
|
# CSP even across Turbo body swaps (see WidgetsController).
|
|
6
6
|
get 'widget.js', to: 'widgets#show', as: :widget
|
|
7
|
+
get 'dashboard.css', to: 'widgets#dashboard_stylesheet', as: :dashboard_stylesheet
|
|
7
8
|
|
|
8
9
|
# index is the read-only admin dashboard; create + the `context` collection
|
|
9
10
|
# route are the widget's public API (see SuggestionsController). There is
|
|
@@ -47,7 +47,8 @@ I18nProofreading.configure do |config|
|
|
|
47
47
|
# config.show_pill = true
|
|
48
48
|
# config.toggle_param = "i18n_proofreading"
|
|
49
49
|
|
|
50
|
-
#
|
|
50
|
+
# Default mount path used by `mount_i18n_proofreading`.
|
|
51
|
+
# Override only if you mount the engine manually.
|
|
51
52
|
#
|
|
52
53
|
# config.mount_path = "/i18n_proofreading"
|
|
53
54
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
4
|
+
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff; --code: #f0f1f3;
|
|
5
|
+
--pending: #d97706; --applied: #16a34a; --rejected: #6b7280;
|
|
6
|
+
}
|
|
7
|
+
@media (prefers-color-scheme: dark) {
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
10
|
+
--border: #2a313a; --accent: #3b82f6; --code: #232a33;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body {
|
|
15
|
+
margin: 0; background: var(--bg); color: var(--text);
|
|
16
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
17
|
+
}
|
|
18
|
+
a { color: var(--accent); text-decoration: none; }
|
|
19
|
+
a:hover { text-decoration: underline; }
|
|
20
|
+
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
21
|
+
h1 { font-size: 22px; margin: 0; }
|
|
22
|
+
.lede { color: var(--muted); margin: 4px 0 0; }
|
|
23
|
+
header.page { margin-bottom: 20px; }
|
|
24
|
+
|
|
25
|
+
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
26
|
+
.tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
|
|
27
|
+
.tabs a.active {
|
|
28
|
+
color: var(--text); font-weight: 600; border: 1px solid var(--border);
|
|
29
|
+
border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px;
|
|
30
|
+
}
|
|
31
|
+
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
32
|
+
.count {
|
|
33
|
+
display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
34
|
+
background: var(--border); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
|
|
38
|
+
.filters label { color: var(--muted); font-size: 13px; }
|
|
39
|
+
.filters select {
|
|
40
|
+
padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
|
|
41
|
+
background: var(--surface); color: var(--text); font: inherit;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.card {
|
|
45
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
46
|
+
padding: 14px 16px; margin-bottom: 12px;
|
|
47
|
+
}
|
|
48
|
+
.row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
49
|
+
code.key {
|
|
50
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
|
|
51
|
+
background: var(--code); padding: 2px 7px; border-radius: 6px; overflow-wrap: anywhere;
|
|
52
|
+
}
|
|
53
|
+
.badge {
|
|
54
|
+
display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
|
|
55
|
+
color: #fff; white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
.badge.status-pending { background: var(--pending); }
|
|
58
|
+
.badge.status-applied { background: var(--applied); }
|
|
59
|
+
.badge.status-rejected { background: var(--rejected); }
|
|
60
|
+
.badge.locale { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
61
|
+
.spacer { flex: 1; }
|
|
62
|
+
.meta { color: var(--muted); font-size: 13px; }
|
|
63
|
+
|
|
64
|
+
dl.diff { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
65
|
+
dl.diff dt { color: var(--muted); font-size: 13px; }
|
|
66
|
+
dl.diff dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
67
|
+
dd .old { color: var(--muted); }
|
|
68
|
+
dd .new { font-weight: 600; }
|
|
69
|
+
.comment { margin-top: 10px; padding: 8px 10px; background: var(--code); border-radius: 8px; font-size: 14px; overflow-wrap: anywhere; }
|
|
70
|
+
|
|
71
|
+
button {
|
|
72
|
+
padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
73
|
+
background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 600;
|
|
74
|
+
}
|
|
75
|
+
button:hover { border-color: var(--muted); }
|
|
76
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
77
|
+
|
|
78
|
+
.empty { padding: 48px 16px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; }
|
|
79
|
+
.pager { margin-top: 16px; display: flex; justify-content: space-between; }
|
|
80
|
+
.pager .muted { color: var(--muted); }
|
|
@@ -22,5 +22,14 @@ module I18nProofreading
|
|
|
22
22
|
include I18nProofreading::TagHelper
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
initializer 'i18n_proofreading.routing' do
|
|
27
|
+
ActionDispatch::Routing::Mapper.include(Module.new do
|
|
28
|
+
def mount_i18n_proofreading(at: I18nProofreading.config.mount_path, **options)
|
|
29
|
+
I18nProofreading.config.mount_path = at
|
|
30
|
+
mount I18nProofreading::Engine, at:, **options
|
|
31
|
+
end
|
|
32
|
+
end)
|
|
33
|
+
end
|
|
25
34
|
end
|
|
26
35
|
end
|
|
@@ -15,6 +15,7 @@ module I18nProofreading
|
|
|
15
15
|
# app/assets/* directory, which would put this file in the host's asset
|
|
16
16
|
# namespace as a bare "widget.js" and into its precompiled output.
|
|
17
17
|
SOURCE = File.expand_path('widget.js', __dir__)
|
|
18
|
+
DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__)
|
|
18
19
|
|
|
19
20
|
# Right-to-left scripts, so the popover renders mirrored for those locales.
|
|
20
21
|
# Matched on the language subtag, so region variants ("ar-EG") count too.
|
|
@@ -25,6 +26,10 @@ module I18nProofreading
|
|
|
25
26
|
@javascript ||= File.read(SOURCE)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def dashboard_stylesheet
|
|
30
|
+
@dashboard_stylesheet ||= File.read(DASHBOARD_STYLESHEET_SOURCE)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# Content fingerprint for the cache-busting script URL: a changed file is
|
|
29
34
|
# a changed URL, so no browser can ever run stale widget code — Safari
|
|
30
35
|
# has been caught ignoring must-revalidate on same-URL scripts.
|
|
@@ -32,6 +37,10 @@ module I18nProofreading
|
|
|
32
37
|
@fingerprint ||= Digest::MD5.hexdigest(javascript)
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
def dashboard_stylesheet_fingerprint
|
|
41
|
+
@dashboard_stylesheet_fingerprint ||= Digest::MD5.hexdigest(dashboard_stylesheet)
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
# The two <script> tags to place before </body>.
|
|
36
45
|
#
|
|
37
46
|
# The config rides in a `type="application/json"` block: it is *data*, not
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: i18n_proofreading
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -78,6 +78,7 @@ files:
|
|
|
78
78
|
- lib/generators/i18n_proofreading/install/templates/initializer.rb
|
|
79
79
|
- lib/i18n_proofreading.rb
|
|
80
80
|
- lib/i18n_proofreading/configuration.rb
|
|
81
|
+
- lib/i18n_proofreading/dashboard.css
|
|
81
82
|
- lib/i18n_proofreading/engine.rb
|
|
82
83
|
- lib/i18n_proofreading/marking.rb
|
|
83
84
|
- lib/i18n_proofreading/middleware.rb
|