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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 332c3bc0e3ee49fd7f316be88b3f4b12d929f646918cfde3d5219f6aefe2b13d
4
- data.tar.gz: d0728318f2e8250f10b4e36cbcde09e540cf81a51fb57db39abf1e66e8990a02
3
+ metadata.gz: c234f2ba39e1d73f3ed201be05d4cce85a6550977b956943158fe353c9f13b50
4
+ data.tar.gz: 72a56aba394043a0300cb1500c877b9dd0b2eb6e33de2085b83948cdc5a1d9a8
5
5
  SHA512:
6
- metadata.gz: a31a61e1e72ef9e8ad6037bea224e7116ed888f6d09af5fe2872bf45941a0e4da67f7587641dc54445b093e01097eab185115909664646ff3dd4e6d956edff52
7
- data.tar.gz: b12ee534fdf28b90cc8d123594a473c5d0c8b67daeefc66b34258235cb0516fc04cdcfef4eac5bf9b6998bb1efd1e4ff3e40c34cdc547a8d54e149148e7df5eb
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
- # The script is static and carries no user data; without this, Rails'
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
- expires_in 1.year, public: true if params[:v] == Widget.fingerprint
21
- return unless stale?(etag: [I18nProofreading::VERSION, Widget.fingerprint])
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: Widget.javascript, content_type: 'text/javascript'
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
- <style>
9
- :root {
10
- color-scheme: light dark;
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
@@ -22,7 +22,7 @@ module I18nProofreading
22
22
  end
23
23
 
24
24
  def mount_engine
25
- route %(mount I18nProofreading::Engine => "/i18n_proofreading")
25
+ route %(mount_i18n_proofreading at: "/i18n_proofreading")
26
26
  end
27
27
 
28
28
  def post_install
@@ -47,7 +47,8 @@ I18nProofreading.configure do |config|
47
47
  # config.show_pill = true
48
48
  # config.toggle_param = "i18n_proofreading"
49
49
 
50
- # Keep this in sync with the `mount` line in config/routes.rb.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18nProofreading
4
- VERSION = '0.10.2'
4
+ VERSION = '0.10.3'
5
5
  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.2
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