solid_stack_web 1.5.0 → 1.6.1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +157 -3
  3. data/app/assets/stylesheets/solid_stack_web/_02_layout.css +12 -0
  4. data/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +4 -3
  5. data/app/controllers/solid_stack_web/application_controller.rb +10 -0
  6. data/app/controllers/solid_stack_web/audit_controller.rb +1 -1
  7. data/app/controllers/solid_stack_web/cable/channel_purges_controller.rb +1 -1
  8. data/app/controllers/solid_stack_web/cable/purges_controller.rb +1 -1
  9. data/app/controllers/solid_stack_web/cache/flushes_controller.rb +1 -1
  10. data/app/controllers/solid_stack_web/cache_entries_controller.rb +1 -1
  11. data/app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb +3 -3
  12. data/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +4 -4
  13. data/app/controllers/solid_stack_web/failed_jobs_controller.rb +2 -2
  14. data/app/controllers/solid_stack_web/jobs/selections_controller.rb +2 -2
  15. data/app/controllers/solid_stack_web/jobs_controller.rb +2 -2
  16. data/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb +4 -4
  17. data/app/controllers/solid_stack_web/scheduled_jobs_controller.rb +5 -5
  18. data/app/helpers/solid_stack_web/application_helper.rb +41 -17
  19. data/app/views/layouts/solid_stack_web/application.html.erb +28 -24
  20. data/app/views/solid_stack_web/audit/index.html.erb +18 -18
  21. data/app/views/solid_stack_web/cable/index.html.erb +24 -21
  22. data/app/views/solid_stack_web/cable_messages/index.html.erb +18 -17
  23. data/app/views/solid_stack_web/cache/index.html.erb +19 -19
  24. data/app/views/solid_stack_web/cache_entries/index.html.erb +16 -15
  25. data/app/views/solid_stack_web/cache_entries/show.html.erb +11 -11
  26. data/app/views/solid_stack_web/dashboard/index.html.erb +54 -33
  27. data/app/views/solid_stack_web/errors/internal_server_error.html.erb +4 -4
  28. data/app/views/solid_stack_web/errors/not_found.html.erb +4 -4
  29. data/app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb +2 -2
  30. data/app/views/solid_stack_web/failed_jobs/errors/index.html.erb +10 -10
  31. data/app/views/solid_stack_web/failed_jobs/index.html.erb +22 -22
  32. data/app/views/solid_stack_web/failed_jobs/show.html.erb +16 -16
  33. data/app/views/solid_stack_web/history/index.html.erb +19 -18
  34. data/app/views/solid_stack_web/jobs/_empty.html.erb +8 -8
  35. data/app/views/solid_stack_web/jobs/index.html.erb +35 -34
  36. data/app/views/solid_stack_web/jobs/show.html.erb +16 -16
  37. data/app/views/solid_stack_web/processes/index.html.erb +8 -8
  38. data/app/views/solid_stack_web/queues/index.html.erb +13 -13
  39. data/app/views/solid_stack_web/queues/show.html.erb +16 -16
  40. data/app/views/solid_stack_web/recurring_tasks/index.html.erb +16 -16
  41. data/app/views/solid_stack_web/shared/_locale_switcher.html.erb +14 -0
  42. data/app/views/solid_stack_web/stats/index.html.erb +13 -13
  43. data/config/locales/en.yml +395 -0
  44. data/config/locales/es.yml +395 -0
  45. data/lib/solid_stack_web/engine.rb +1 -0
  46. data/lib/solid_stack_web/version.rb +1 -1
  47. data/lib/solid_stack_web.rb +14 -1
  48. metadata +5 -2
@@ -21,6 +21,17 @@ module SolidStackWeb
21
21
  { label: "Text", content: str }
22
22
  end
23
23
 
24
+ # Solid Cable stores payloads (and channel names) in a binary (bytea) column,
25
+ # so ActiveRecord hands the view a string with ASCII-8BIT (BINARY) encoding.
26
+ # Appending such a value — once it holds non-ASCII bytes (e.g. a "→" in a
27
+ # Turbo Stream payload) and the UTF-8 output buffer already contains
28
+ # non-ASCII text — raises Encoding::CompatibilityError. Re-tag the bytes as
29
+ # UTF-8 (lossless; the underlying data is UTF-8 source text) before it
30
+ # reaches the view. dup keeps the ActiveRecord attribute string unmutated.
31
+ def to_utf8_text(value)
32
+ value.to_s.dup.force_encoding("UTF-8").scrub
33
+ end
34
+
24
35
  def format_duration(seconds)
25
36
  return "—" if seconds.nil?
26
37
  return "#{(seconds * 1000).round}ms" if seconds < 1
@@ -35,11 +46,14 @@ module SolidStackWeb
35
46
  build_sparkline_svg(
36
47
  Struct.new(:buckets, :max).new(timeline.entry_buckets, timeline.entry_max),
37
48
  css_class: "sqw-sparkline sqw-sparkline--lg",
38
- aria_label: "Cache entries written over the last 24 hours"
49
+ aria_label: t("solid_stack_web.helpers.cache_entry_this_hour", count: 0).gsub(/\d+/, "…")
39
50
  ) do |count, i|
40
51
  hours_ago = CacheTimeline::HOURS - 1 - i
41
- hours_ago.zero? ? "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} this hour" \
42
- : "#{count} #{"entry".then { |w| count == 1 ? w : "entries" }} #{hours_ago}h ago"
52
+ if hours_ago.zero?
53
+ t("solid_stack_web.helpers.cache_entry_this_hour", count: count)
54
+ else
55
+ t("solid_stack_web.helpers.cache_entry_hours_ago", count: count, hours: hours_ago)
56
+ end
43
57
  end
44
58
  end
45
59
 
@@ -47,11 +61,15 @@ module SolidStackWeb
47
61
  build_sparkline_svg(
48
62
  Struct.new(:buckets, :max).new(timeline.byte_buckets, timeline.byte_max),
49
63
  css_class: "sqw-sparkline sqw-sparkline--lg",
50
- aria_label: "Cache bytes written over the last 24 hours"
64
+ aria_label: t("solid_stack_web.cache.bytes_written")
51
65
  ) do |bytes, i|
52
66
  hours_ago = CacheTimeline::HOURS - 1 - i
53
67
  size = number_to_human_size(bytes)
54
- hours_ago.zero? ? "#{size} written this hour" : "#{size} written #{hours_ago}h ago"
68
+ if hours_ago.zero?
69
+ t("solid_stack_web.helpers.cache_size_this_hour", size: size)
70
+ else
71
+ t("solid_stack_web.helpers.cache_size_hours_ago", size: size, hours: hours_ago)
72
+ end
55
73
  end
56
74
  end
57
75
 
@@ -59,31 +77,37 @@ module SolidStackWeb
59
77
  build_sparkline_svg(
60
78
  Struct.new(:buckets, :max).new(timeline.message_buckets, timeline.message_max),
61
79
  css_class: "sqw-sparkline sqw-sparkline--lg",
62
- aria_label: "Cable messages over the last 24 hours"
80
+ aria_label: t("solid_stack_web.cable.messages_timeline")
63
81
  ) do |count, i|
64
82
  hours_ago = CableTimeline::HOURS - 1 - i
65
- label = count == 1 ? "message" : "messages"
66
- hours_ago.zero? ? "#{count} #{label} this hour" : "#{count} #{label} #{hours_ago}h ago"
83
+ if hours_ago.zero?
84
+ t("solid_stack_web.helpers.cable_message_this_hour", count: count)
85
+ else
86
+ t("solid_stack_web.helpers.cable_message_hours_ago", count: count, hours: hours_ago)
87
+ end
67
88
  end
68
89
  end
69
90
 
70
91
  def throughput_sparkline_svg(sparkline)
71
- build_sparkline_svg(sparkline, aria_label: "Throughput over the last 12 hours") do |count, i|
92
+ build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.throughput_label")) do |count, i|
72
93
  hours_ago = SolidStackWeb::ThroughputSparkline::HOURS - i
73
94
  if hours_ago == 1
74
- "#{count} #{count == 1 ? "job" : "jobs"} in the last hour"
95
+ t("solid_stack_web.helpers.throughput_last_hour", count: count)
75
96
  else
76
- "#{count} #{count == 1 ? "job" : "jobs"} (#{hours_ago}h–#{hours_ago - 1}h ago)"
97
+ t("solid_stack_web.helpers.throughput_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
77
98
  end
78
99
  end
79
100
  end
80
101
 
81
102
  def queue_depth_sparkline_svg(sparkline)
82
103
  build_sparkline_svg(sparkline, css_class: "sqw-sparkline sqw-sparkline--sm",
83
- aria_label: "Queue depth over the last 12 hours") do |count, i|
104
+ aria_label: t("solid_stack_web.queues.col_depth")) do |count, i|
84
105
  hours_ago = SolidStackWeb::QueueDepthSparkline::HOURS - 1 - i
85
- jobs_word = count == 1 ? "job" : "jobs"
86
- hours_ago.zero? ? "#{count} ready #{jobs_word} now" : "#{count} ready #{jobs_word} #{hours_ago}h ago"
106
+ if hours_ago.zero?
107
+ t("solid_stack_web.helpers.queue_depth_now", count: count)
108
+ else
109
+ t("solid_stack_web.helpers.queue_depth_hours_ago", count: count, hours: hours_ago)
110
+ end
87
111
  end
88
112
  end
89
113
 
@@ -110,12 +134,12 @@ module SolidStackWeb
110
134
  end
111
135
 
112
136
  def failed_job_sparkline_svg(sparkline)
113
- build_sparkline_svg(sparkline, aria_label: "Failed jobs over the last 12 hours") do |count, i|
137
+ build_sparkline_svg(sparkline, aria_label: t("solid_stack_web.dashboard.failures_label")) do |count, i|
114
138
  hours_ago = SolidStackWeb::FailedJobSparkline::HOURS - i
115
139
  if hours_ago == 1
116
- "#{count} #{count == 1 ? "failure" : "failures"} in the last hour"
140
+ t("solid_stack_web.helpers.failure_last_hour", count: count)
117
141
  else
118
- "#{count} #{count == 1 ? "failure" : "failures"} (#{hours_ago}h–#{hours_ago - 1}h ago)"
142
+ t("solid_stack_web.helpers.failure_hours_ago", count: count, from: hours_ago, to: hours_ago - 1)
119
143
  end
120
144
  end
121
145
  end
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="<%= I18n.locale %>">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Solid Stack Dashboard</title>
6
+ <title><%= t("solid_stack_web.layout.title") %></title>
7
7
  <link rel="icon" href="data:,">
8
8
  <%= csrf_meta_tags %>
9
9
  <%= csp_meta_tag %>
@@ -11,61 +11,65 @@
11
11
  <%= javascript_importmap_tags "solid_stack_web" %>
12
12
  </head>
13
13
  <body data-controller="theme">
14
- <a href="#main-content" class="sqw-skip-link">Skip to main content</a>
14
+ <a href="#main-content" class="sqw-skip-link"><%= t("solid_stack_web.layout.skip_to_content") %></a>
15
15
  <header class="sqw-header">
16
16
  <div class="sqw-header__inner">
17
- <%= link_to "Solid Stack", root_path, class: "sqw-header__logo" %>
18
- <nav class="sqw-nav" aria-label="Main navigation">
19
- <%= link_to "Queue", jobs_path,
17
+ <%= link_to t("solid_stack_web.layout.logo"), root_path, class: "sqw-header__logo" %>
18
+ <nav class="sqw-nav" aria-label="<%= t("solid_stack_web.layout.nav.main_navigation") %>">
19
+ <%= link_to t("solid_stack_web.layout.nav.queue"), jobs_path,
20
20
  class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :queue}" %>
21
- <%= link_to "Cache", cache_path,
21
+ <%= link_to t("solid_stack_web.layout.nav.cache"), cache_path,
22
22
  class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cache}" %>
23
- <%= link_to "Cable", cable_path,
23
+ <%= link_to t("solid_stack_web.layout.nav.cable"), cable_path,
24
24
  class: "sqw-nav__link#{" sqw-nav__link--active" if current_section == :cable}" %>
25
+ <% SolidStackWeb.nav_links.each do |link| %>
26
+ <%= link_to link[:label], link[:url], class: "sqw-nav__link" %>
27
+ <% end %>
25
28
  </nav>
26
- <button class="sqw-theme-toggle" aria-label="Switch to dark mode"
29
+ <%= render "solid_stack_web/shared/locale_switcher" %>
30
+ <button class="sqw-theme-toggle" aria-label="<%= t("solid_stack_web.layout.theme_toggle") %>"
27
31
  data-theme-target="toggle" data-action="theme#toggle">☽</button>
28
32
  </div>
29
33
  </header>
30
34
 
31
35
  <% if current_section == :cache %>
32
- <nav class="sqw-subnav" aria-label="Cache section">
36
+ <nav class="sqw-subnav" aria-label="<%= t("solid_stack_web.layout.subnav.cache_section") %>">
33
37
  <div class="sqw-subnav__inner">
34
- <%= link_to "Overview", cache_path,
38
+ <%= link_to t("solid_stack_web.layout.subnav.overview"), cache_path,
35
39
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cache"}" %>
36
- <%= link_to "Entries", cache_entries_path,
40
+ <%= link_to t("solid_stack_web.layout.subnav.entries"), cache_entries_path,
37
41
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cache_entries"}" %>
38
42
  </div>
39
43
  </nav>
40
44
  <% end %>
41
45
 
42
46
  <% if current_section == :cable %>
43
- <nav class="sqw-subnav" aria-label="Cable section">
47
+ <nav class="sqw-subnav" aria-label="<%= t("solid_stack_web.layout.subnav.cable_section") %>">
44
48
  <div class="sqw-subnav__inner">
45
- <%= link_to "Overview", cable_path,
49
+ <%= link_to t("solid_stack_web.layout.subnav.overview"), cable_path,
46
50
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "cable"}" %>
47
51
  </div>
48
52
  </nav>
49
53
  <% end %>
50
54
 
51
55
  <% if current_section == :queue %>
52
- <nav class="sqw-subnav" aria-label="Queue section">
56
+ <nav class="sqw-subnav" aria-label="<%= t("solid_stack_web.layout.subnav.queue_section") %>">
53
57
  <div class="sqw-subnav__inner">
54
- <%= link_to "Jobs", jobs_path,
58
+ <%= link_to t("solid_stack_web.layout.subnav.jobs"), jobs_path,
55
59
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "jobs"}" %>
56
- <%= link_to "Failed", failed_jobs_path,
60
+ <%= link_to t("solid_stack_web.layout.subnav.failed"), failed_jobs_path,
57
61
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
58
- <%= link_to "Queues", queues_path,
62
+ <%= link_to t("solid_stack_web.layout.subnav.queues"), queues_path,
59
63
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
60
- <%= link_to "Recurring", recurring_tasks_path,
64
+ <%= link_to t("solid_stack_web.layout.subnav.recurring"), recurring_tasks_path,
61
65
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "recurring_tasks"}" %>
62
- <%= link_to "Stats", stats_path,
66
+ <%= link_to t("solid_stack_web.layout.subnav.stats"), stats_path,
63
67
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "stats"}" %>
64
- <%= link_to "History", history_path,
68
+ <%= link_to t("solid_stack_web.layout.subnav.history"), history_path,
65
69
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %>
66
- <%= link_to "Processes", processes_path,
70
+ <%= link_to t("solid_stack_web.layout.subnav.processes"), processes_path,
67
71
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
68
- <%= link_to "Audit", audit_path,
72
+ <%= link_to t("solid_stack_web.layout.subnav.audit"), audit_path,
69
73
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "audit"}" %>
70
74
  </div>
71
75
  </nav>
@@ -80,4 +84,4 @@
80
84
  <%= yield %>
81
85
  </main>
82
86
  </body>
83
- </html>
87
+ </html>
@@ -1,16 +1,16 @@
1
1
  <div class="sqw-page-header sqw-page-header--split">
2
- <h1 class="sqw-page-title">Audit Log</h1>
2
+ <h1 class="sqw-page-title"><%= t("solid_stack_web.audit.title") %></h1>
3
3
  <% if @events&.any? %>
4
- <%= link_to "Export CSV",
4
+ <%= link_to t("solid_stack_web.audit.export_csv"),
5
5
  audit_path(format: :csv, audit_action: @action_filter, actor: @actor_filter, queue: @queue_filter),
6
6
  class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
7
7
  <% end %>
8
8
  </div>
9
9
 
10
10
  <form class="sqw-filters" action="<%= audit_path %>" method="get">
11
- <select name="audit_action" class="sqw-select" aria-label="Filter by action"
11
+ <select name="audit_action" class="sqw-select" aria-label="<%= t("solid_stack_web.audit.aria_filter_action") %>"
12
12
  onchange="this.form.submit()">
13
- <option value="">All actions</option>
13
+ <option value=""><%= t("solid_stack_web.audit.all_actions") %></option>
14
14
  <% SolidStackWeb::AuditEvent::ACTIONS.each do |action| %>
15
15
  <option value="<%= action %>" <%= "selected" if @action_filter == action %>><%= action.tr("_", " ") %></option>
16
16
  <% end %>
@@ -18,20 +18,20 @@
18
18
 
19
19
  <% if @actor_filter.present? %>
20
20
  <span class="sqw-badge">
21
- Actor: <%= @actor_filter %>
22
- <%= link_to "×", audit_path(audit_action: @action_filter, queue: @queue_filter), class: "sqw-muted", aria: { label: "Clear actor filter" } %>
21
+ <%= t("solid_stack_web.audit.actor_label") %> <%= @actor_filter %>
22
+ <%= link_to "×", audit_path(audit_action: @action_filter, queue: @queue_filter), class: "sqw-muted", aria: { label: t("solid_stack_web.audit.aria_clear_actor") } %>
23
23
  </span>
24
24
  <% end %>
25
25
 
26
26
  <% if @queue_filter.present? %>
27
27
  <span class="sqw-badge">
28
- Queue: <%= @queue_filter %>
29
- <%= link_to "×", audit_path(audit_action: @action_filter, actor: @actor_filter), class: "sqw-muted", aria: { label: "Clear queue filter" } %>
28
+ <%= t("solid_stack_web.audit.queue_label") %> <%= @queue_filter %>
29
+ <%= link_to "×", audit_path(audit_action: @action_filter, actor: @actor_filter), class: "sqw-muted", aria: { label: t("solid_stack_web.audit.aria_clear_queue") } %>
30
30
  </span>
31
31
  <% end %>
32
32
 
33
33
  <% if @action_filter.present? || @actor_filter.present? || @queue_filter.present? %>
34
- <%= link_to "Clear all", audit_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
34
+ <%= link_to t("solid_stack_web.audit.clear_all"), audit_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
35
35
  <% end %>
36
36
  </form>
37
37
 
@@ -39,12 +39,12 @@
39
39
  <table class="sqw-table">
40
40
  <thead>
41
41
  <tr>
42
- <th scope="col">Time</th>
43
- <th scope="col">Action</th>
44
- <th scope="col">Actor</th>
45
- <th scope="col">Job Class</th>
46
- <th scope="col">Queue</th>
47
- <th scope="col">Count</th>
42
+ <th scope="col"><%= t("solid_stack_web.audit.col_time") %></th>
43
+ <th scope="col"><%= t("solid_stack_web.audit.col_action") %></th>
44
+ <th scope="col"><%= t("solid_stack_web.audit.col_actor") %></th>
45
+ <th scope="col"><%= t("solid_stack_web.audit.col_job_class") %></th>
46
+ <th scope="col"><%= t("solid_stack_web.audit.col_queue") %></th>
47
+ <th scope="col"><%= t("solid_stack_web.audit.col_count") %></th>
48
48
  </tr>
49
49
  </thead>
50
50
  <tbody>
@@ -78,9 +78,9 @@
78
78
  <% end %>
79
79
  </tbody>
80
80
  </table>
81
- <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
81
+ <%== @pagy.series_nav(aria_label: t("solid_stack_web.shared.pagination")) if @pagy.pages > 1 %>
82
82
  <% else %>
83
83
  <div class="sqw-empty" id="sqd-empty">
84
- <p class="sqw-muted">No audit events recorded yet.</p>
84
+ <p class="sqw-muted"><%= t("solid_stack_web.audit.empty") %></p>
85
85
  </div>
86
- <% end %>
86
+ <% end %>
@@ -1,48 +1,51 @@
1
1
  <div class="sqw-page-header sqw-page-header--split">
2
- <h1 class="sqw-page-title">Solid Cable</h1>
2
+ <h1 class="sqw-page-title"><%= t("solid_stack_web.cable.title") %></h1>
3
3
  <div class="sqw-header-actions">
4
4
  <%= form_with url: cable_purge_path, method: :delete, class: "sqw-inline-form",
5
- data: { turbo_confirm: "Purge these messages? This cannot be undone." } do |f| %>
5
+ data: { turbo_confirm: t("solid_stack_web.cable.confirm_purge") } do |f| %>
6
6
  <%= f.select :older_than,
7
- [["Older than 1 day", 1], ["Older than 7 days", 7], ["Older than 30 days", 30]],
7
+ [[t("solid_stack_web.cable.older_than_1_day"), 1],
8
+ [t("solid_stack_web.cable.older_than_7_days"), 7],
9
+ [t("solid_stack_web.cable.older_than_30_days"), 30]],
8
10
  {}, class: "sqw-select" %>
9
- <%= f.submit "Purge Old", class: "sqw-btn sqw-btn--danger sqw-btn--sm" %>
11
+ <%= f.submit t("solid_stack_web.cable.purge_old"), class: "sqw-btn sqw-btn--danger sqw-btn--sm" %>
10
12
  <% end %>
11
13
  </div>
12
14
  </div>
13
15
 
14
16
  <div class="sqw-stats-grid">
15
17
  <div class="sqw-stat sqw-stat--cable">
16
- <span class="sqw-stat__label">Total Messages</span>
18
+ <span class="sqw-stat__label"><%= t("solid_stack_web.cable.total_messages") %></span>
17
19
  <span class="sqw-stat__value"><%= @total_messages %></span>
18
20
  </div>
19
21
  <div class="sqw-stat sqw-stat--cable">
20
- <span class="sqw-stat__label">Channels</span>
22
+ <span class="sqw-stat__label"><%= t("solid_stack_web.cable.channels") %></span>
21
23
  <span class="sqw-stat__value"><%= @channels.size %></span>
22
24
  </div>
23
25
  </div>
24
26
 
25
27
  <div class="sqw-timeline-grid sqw-timeline-grid--single" data-controller="sparkline-tooltip">
26
28
  <div class="sqw-sparkline-wrap sqw-timeline-chart sqw-timeline-chart--cable">
27
- <span class="sqw-sparkline-label">Messages — last 24 hours</span>
29
+ <span class="sqw-sparkline-label"><%= t("solid_stack_web.cable.messages_timeline") %></span>
28
30
  <div class="sqw-sparkline-positioner">
29
31
  <%= cable_messages_timeline_svg(@timeline) %>
30
32
  <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
31
33
  </div>
32
34
  <div class="sqw-sparkline-axis">
33
- <span>24h ago</span>
34
- <span>12h ago</span>
35
- <span>now</span>
35
+ <span><%= t("solid_stack_web.cable.axis_24h_ago") %></span>
36
+ <span><%= t("solid_stack_web.cable.axis_12h_ago") %></span>
37
+ <span><%= t("solid_stack_web.cable.axis_now") %></span>
36
38
  </div>
37
39
  </div>
38
40
  </div>
39
41
 
40
42
  <form class="sqw-filters" action="<%= cable_path %>" method="get" data-controller="search">
41
43
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
42
- placeholder="Filter by channel…" autocomplete="off" aria-label="Filter by channel"
44
+ placeholder="<%= t("solid_stack_web.cable.placeholder_channel") %>" autocomplete="off"
45
+ aria-label="<%= t("solid_stack_web.cable.aria_filter_channel") %>"
43
46
  data-action="input->search#filter">
44
47
  <% if @search.present? %>
45
- <%= link_to "Clear", cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
48
+ <%= link_to t("solid_stack_web.shared.clear"), cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
46
49
  <% end %>
47
50
  </form>
48
51
 
@@ -50,16 +53,16 @@
50
53
  <table class="sqw-table">
51
54
  <thead>
52
55
  <tr>
53
- <th scope="col">Channel</th>
54
- <th scope="col">Messages</th>
55
- <th scope="col">Last Message</th>
56
+ <th scope="col"><%= t("solid_stack_web.cable.col_channel") %></th>
57
+ <th scope="col"><%= t("solid_stack_web.cable.col_messages") %></th>
58
+ <th scope="col"><%= t("solid_stack_web.cable.col_last_message") %></th>
56
59
  </tr>
57
60
  </thead>
58
61
  <tbody>
59
62
  <% @channels.each do |channel| %>
60
63
  <tr>
61
- <td class="sqw-monospace sqw-truncate" title="<%= channel[:channel] %>">
62
- <%= link_to channel[:channel], cable_channel_messages_path(channel[:channel_hash]), class: "sqw-link" %>
64
+ <td class="sqw-monospace sqw-truncate" title="<%= to_utf8_text(channel[:channel]) %>">
65
+ <%= link_to to_utf8_text(channel[:channel]), cable_channel_messages_path(channel[:channel_hash]), class: "sqw-link" %>
63
66
  </td>
64
67
  <td><%= channel[:message_count] %></td>
65
68
  <td class="sqw-muted"><%= local_time(channel[:last_message_at]) %></td>
@@ -70,11 +73,11 @@
70
73
  <% else %>
71
74
  <div class="sqw-empty">
72
75
  <% if @search.present? %>
73
- <p class="sqw-empty__title">No channels matching &ldquo;<%= @search %>&rdquo;</p>
74
- <p class="sqw-empty__hint"><%= link_to "Clear search", cable_path %></p>
76
+ <p class="sqw-empty__title"><%= t("solid_stack_web.cable.no_channels_matching", search: @search) %></p>
77
+ <p class="sqw-empty__hint"><%= link_to t("solid_stack_web.cable.clear_search"), cable_path %></p>
75
78
  <% else %>
76
- <p class="sqw-empty__title">No cable messages</p>
77
- <p class="sqw-empty__hint">Messages will appear here once clients connect and broadcast over Action Cable.</p>
79
+ <p class="sqw-empty__title"><%= t("solid_stack_web.cable.empty_title") %></p>
80
+ <p class="sqw-empty__hint"><%= t("solid_stack_web.cable.empty_hint") %></p>
78
81
  <% end %>
79
82
  </div>
80
83
  <% end %>
@@ -1,21 +1,22 @@
1
1
  <div class="sqw-page-header sqw-page-header--split">
2
- <h1 class="sqw-page-title sqw-truncate" title="<%= @channel_name %>"><%= @channel_name %></h1>
2
+ <h1 class="sqw-page-title sqw-truncate" title="<%= to_utf8_text(@channel_name) %>"><%= to_utf8_text(@channel_name) %></h1>
3
3
  <div class="sqw-header-actions">
4
- <%= button_to "Purge Channel",
4
+ <%= button_to t("solid_stack_web.cable_messages.purge_channel"),
5
5
  cable_channel_purge_path(params[:channel_hash]),
6
6
  method: :delete,
7
7
  class: "sqw-btn sqw-btn--danger sqw-btn--sm",
8
- data: { turbo_confirm: "Delete all messages for this channel? This cannot be undone." } %>
9
- <%= link_to "← Channels", cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
8
+ data: { turbo_confirm: t("solid_stack_web.cable_messages.confirm_purge_channel") } %>
9
+ <%= link_to t("solid_stack_web.cable_messages.back_to_channels"), cable_path, class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
10
10
  </div>
11
11
  </div>
12
12
 
13
13
  <form class="sqw-filters" action="<%= cable_channel_messages_path(params[:channel_hash]) %>" method="get" data-controller="search">
14
14
  <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
15
- placeholder="Filter by payload…" autocomplete="off" aria-label="Filter by payload"
15
+ placeholder="<%= t("solid_stack_web.cable_messages.placeholder_payload") %>" autocomplete="off"
16
+ aria-label="<%= t("solid_stack_web.cable_messages.aria_filter_payload") %>"
16
17
  data-action="input->search#filter">
17
18
  <% if @search.present? %>
18
- <%= link_to "Clear", cable_channel_messages_path(params[:channel_hash]), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
19
+ <%= link_to t("solid_stack_web.shared.clear"), cable_channel_messages_path(params[:channel_hash]), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
19
20
  <% end %>
20
21
  </form>
21
22
 
@@ -23,17 +24,17 @@
23
24
  <table class="sqw-table">
24
25
  <thead>
25
26
  <tr>
26
- <th scope="col">ID</th>
27
- <th scope="col">Payload</th>
28
- <th scope="col">Sent</th>
27
+ <th scope="col"><%= t("solid_stack_web.cable_messages.col_id") %></th>
28
+ <th scope="col"><%= t("solid_stack_web.cable_messages.col_payload") %></th>
29
+ <th scope="col"><%= t("solid_stack_web.cable_messages.col_sent") %></th>
29
30
  </tr>
30
31
  </thead>
31
32
  <tbody>
32
33
  <% @messages.each do |message| %>
33
34
  <tr>
34
35
  <td class="sqw-muted sqw-monospace"><%= message.id %></td>
35
- <td class="sqw-monospace sqw-truncate" title="<%= message.payload %>">
36
- <%= truncate(message.payload.to_s, length: 120) %>
36
+ <td class="sqw-monospace sqw-truncate" title="<%= to_utf8_text(message.payload) %>">
37
+ <%= truncate(to_utf8_text(message.payload), length: 120) %>
37
38
  </td>
38
39
  <td class="sqw-muted">
39
40
  <%= local_time(message.created_at, format: :relative) %>
@@ -42,15 +43,15 @@
42
43
  <% end %>
43
44
  </tbody>
44
45
  </table>
45
- <%== @pagy.series_nav(aria_label: "Pagination") if @pagy.pages > 1 %>
46
+ <%== @pagy.series_nav(aria_label: t("solid_stack_web.shared.pagination")) if @pagy.pages > 1 %>
46
47
  <% else %>
47
48
  <div class="sqw-empty">
48
49
  <% if @search.present? %>
49
- <p class="sqw-empty__title">No messages matching &ldquo;<%= @search %>&rdquo;</p>
50
- <p class="sqw-empty__hint"><%= link_to "Clear search", cable_channel_messages_path(params[:channel_hash]) %></p>
50
+ <p class="sqw-empty__title"><%= t("solid_stack_web.cable_messages.no_messages_matching", search: @search) %></p>
51
+ <p class="sqw-empty__hint"><%= link_to t("solid_stack_web.cable_messages.clear_search"), cable_channel_messages_path(params[:channel_hash]) %></p>
51
52
  <% else %>
52
- <p class="sqw-empty__title">No messages for this channel</p>
53
- <p class="sqw-empty__hint">Messages may have been purged or the channel has gone quiet. <%= link_to "Back to channels →", cable_path %></p>
53
+ <p class="sqw-empty__title"><%= t("solid_stack_web.cable_messages.empty_title") %></p>
54
+ <p class="sqw-empty__hint"><%= t("solid_stack_web.cable_messages.empty_hint") %> <%= link_to t("solid_stack_web.cable_messages.back_to_channels_link"), cable_path %></p>
54
55
  <% end %>
55
56
  </div>
56
- <% end %>
57
+ <% end %>
@@ -1,14 +1,14 @@
1
1
  <div class="sqw-page-header">
2
- <h1 class="sqw-page-title">Solid Cache</h1>
2
+ <h1 class="sqw-page-title"><%= t("solid_stack_web.cache.title") %></h1>
3
3
  </div>
4
4
 
5
5
  <div class="sqw-stats-grid">
6
6
  <%= link_to cache_entries_path, class: "sqw-stat sqw-stat--cache" do %>
7
- <span class="sqw-stat__label">Total Entries</span>
7
+ <span class="sqw-stat__label"><%= t("solid_stack_web.cache.total_entries") %></span>
8
8
  <span class="sqw-stat__value"><%= @total_entries %></span>
9
9
  <% end %>
10
10
  <div class="sqw-stat sqw-stat--cache">
11
- <span class="sqw-stat__label">Total Size</span>
11
+ <span class="sqw-stat__label"><%= t("solid_stack_web.cache.total_size") %></span>
12
12
  <span class="sqw-stat__value"><%= number_to_human_size(@total_byte_size) %></span>
13
13
  </div>
14
14
  </div>
@@ -16,13 +16,13 @@
16
16
  <% if @total_entries > 0 %>
17
17
  <div class="sqw-size-grid">
18
18
  <section class="sqw-size-section">
19
- <h2 class="sqw-section-title">Size Distribution</h2>
19
+ <h2 class="sqw-section-title"><%= t("solid_stack_web.cache.size_distribution") %></h2>
20
20
  <table class="sqw-table">
21
21
  <thead>
22
22
  <tr>
23
- <th scope="col">Range</th>
24
- <th scope="col">Entries</th>
25
- <th scope="col"><span class="sqw-sr-only">Distribution</span></th>
23
+ <th scope="col"><%= t("solid_stack_web.cache.col_range") %></th>
24
+ <th scope="col"><%= t("solid_stack_web.cache.col_entries") %></th>
25
+ <th scope="col"><span class="sqw-sr-only"><%= t("solid_stack_web.cache.col_distribution") %></span></th>
26
26
  </tr>
27
27
  </thead>
28
28
  <tbody>
@@ -44,12 +44,12 @@
44
44
  </section>
45
45
 
46
46
  <section class="sqw-size-section">
47
- <h2 class="sqw-section-title">Largest Entries</h2>
47
+ <h2 class="sqw-section-title"><%= t("solid_stack_web.cache.largest_entries") %></h2>
48
48
  <table class="sqw-table">
49
49
  <thead>
50
50
  <tr>
51
- <th scope="col">Key</th>
52
- <th scope="col">Size</th>
51
+ <th scope="col"><%= t("solid_stack_web.cache.col_key") %></th>
52
+ <th scope="col"><%= t("solid_stack_web.cache.col_size") %></th>
53
53
  </tr>
54
54
  </thead>
55
55
  <tbody>
@@ -69,28 +69,28 @@
69
69
 
70
70
  <div class="sqw-timeline-grid" data-controller="sparkline-tooltip">
71
71
  <div class="sqw-sparkline-wrap sqw-timeline-chart">
72
- <span class="sqw-sparkline-label">Entries written — last 24 hours</span>
72
+ <span class="sqw-sparkline-label"><%= t("solid_stack_web.cache.entries_written") %></span>
73
73
  <div class="sqw-sparkline-positioner">
74
74
  <%= cache_entries_timeline_svg(@timeline) %>
75
75
  <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
76
76
  </div>
77
77
  <div class="sqw-sparkline-axis">
78
- <span>24h ago</span>
79
- <span>12h ago</span>
80
- <span>now</span>
78
+ <span><%= t("solid_stack_web.cache.axis_24h_ago") %></span>
79
+ <span><%= t("solid_stack_web.cache.axis_12h_ago") %></span>
80
+ <span><%= t("solid_stack_web.cache.axis_now") %></span>
81
81
  </div>
82
82
  </div>
83
83
 
84
84
  <div class="sqw-sparkline-wrap sqw-timeline-chart">
85
- <span class="sqw-sparkline-label">Bytes written — last 24 hours</span>
85
+ <span class="sqw-sparkline-label"><%= t("solid_stack_web.cache.bytes_written") %></span>
86
86
  <div class="sqw-sparkline-positioner">
87
87
  <%= cache_bytes_timeline_svg(@timeline) %>
88
88
  <div class="sqw-sparkline-tip" data-sparkline-tooltip-target="tip" hidden></div>
89
89
  </div>
90
90
  <div class="sqw-sparkline-axis">
91
- <span>24h ago</span>
92
- <span>12h ago</span>
93
- <span>now</span>
91
+ <span><%= t("solid_stack_web.cache.axis_24h_ago") %></span>
92
+ <span><%= t("solid_stack_web.cache.axis_12h_ago") %></span>
93
+ <span><%= t("solid_stack_web.cache.axis_now") %></span>
94
94
  </div>
95
95
  </div>
96
- </div>
96
+ </div>