rails_error_dashboard 0.8.3 → 0.9.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +156 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -73
  5. data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
  6. data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
  7. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
  8. data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
  9. data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
  10. data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
  11. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
  12. data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
  13. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
  14. data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
  15. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
  16. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
  17. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
  18. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
  19. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
  20. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
  21. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
  22. data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
  23. data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
  24. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
  25. data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
  26. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
  27. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
  28. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
  29. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
  30. data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
  31. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
  32. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
  33. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
  34. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
  35. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
  36. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
  37. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
  38. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
  39. data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
  40. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
  41. data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
  42. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
  43. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
  44. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
  45. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
  46. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
  47. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
  48. data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
  49. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
  50. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
  51. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
  52. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
  53. data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
  54. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
  55. data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
  56. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
  57. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
  58. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
  59. data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
  60. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
  61. data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
  62. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
  63. data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
  64. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
  65. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +39 -28
  66. data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
  67. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
  68. data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
  69. data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
  70. data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
  71. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
  72. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
  73. data/config/locales/de.yml +2007 -0
  74. data/config/locales/en.yml +2719 -0
  75. data/config/locales/es.yml +2079 -0
  76. data/config/locales/fr.yml +2082 -0
  77. data/config/locales/it.yml +2060 -0
  78. data/config/locales/ja.yml +1802 -0
  79. data/config/locales/pl.yml +2108 -0
  80. data/config/locales/pt-BR.yml +2064 -0
  81. data/config/locales/ru.yml +2109 -0
  82. data/config/locales/uk.yml +2106 -0
  83. data/config/locales/zh-CN.yml +1791 -0
  84. data/config/routes.rb +5 -0
  85. data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
  86. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
  87. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
  88. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
  89. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
  90. data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
  91. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
  92. data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
  93. data/lib/rails_error_dashboard/configuration.rb +23 -0
  94. data/lib/rails_error_dashboard/current.rb +56 -0
  95. data/lib/rails_error_dashboard/i18n_store.rb +293 -0
  96. data/lib/rails_error_dashboard/private_backend.rb +213 -0
  97. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
  98. data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
  99. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
  100. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
  101. data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
  102. data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
  103. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
  104. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
  105. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
  106. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
  107. data/lib/rails_error_dashboard/translation.rb +39 -0
  108. data/lib/rails_error_dashboard/version.rb +1 -1
  109. data/lib/rails_error_dashboard.rb +4 -0
  110. data/lib/tasks/error_dashboard.rake +5 -1
  111. metadata +24 -4
@@ -36,8 +36,8 @@
36
36
  <div class="container">
37
37
  <!-- Header -->
38
38
  <div class="header">
39
- <h1>RED Error Digest</h1>
40
- <div class="subtitle"><%= @digest[:period_label] %> — <%= @digest[:generated_at].strftime('%B %d, %Y') %></div>
39
+ <h1><%= red_t("red.mailers.digest.heading") %></h1>
40
+ <div class="subtitle"><%= @digest[:period_label] %> — <%= red_mail_time(@digest[:generated_at], format: :date_only) %></div>
41
41
  </div>
42
42
 
43
43
  <!-- Summary Stats -->
@@ -45,27 +45,27 @@
45
45
  <div class="stat-grid">
46
46
  <div class="stat-box">
47
47
  <div class="stat-value text-primary"><%= @digest[:stats][:new_errors] %></div>
48
- <div class="stat-label">New Errors</div>
48
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.new_errors") %></div>
49
49
  </div>
50
50
  <div class="stat-box">
51
51
  <div class="stat-value"><%= @digest[:stats][:total_occurrences] %></div>
52
- <div class="stat-label">Occurrences</div>
52
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.total_occurrences") %></div>
53
53
  </div>
54
54
  <div class="stat-box">
55
55
  <div class="stat-value text-success"><%= @digest[:stats][:resolved] %></div>
56
- <div class="stat-label">Resolved</div>
56
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.resolved") %></div>
57
57
  </div>
58
58
  <div class="stat-box">
59
59
  <div class="stat-value text-warning"><%= @digest[:stats][:unresolved] %></div>
60
- <div class="stat-label">Unresolved</div>
60
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.unresolved") %></div>
61
61
  </div>
62
62
  <div class="stat-box">
63
63
  <div class="stat-value text-danger"><%= @digest[:stats][:critical_high] %></div>
64
- <div class="stat-label">Critical/High</div>
64
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.critical_high") %></div>
65
65
  </div>
66
66
  <div class="stat-box">
67
67
  <div class="stat-value"><%= @digest[:stats][:resolution_rate] %>%</div>
68
- <div class="stat-label">Resolution Rate</div>
68
+ <div class="stat-label"><%= red_t("red.mailers.digest.stats.resolution_rate") %></div>
69
69
  </div>
70
70
  </div>
71
71
  </div>
@@ -81,10 +81,10 @@
81
81
  %>
82
82
  <div class="comparison">
83
83
  <div>
84
- <span class="text-muted">vs previous <%= @digest[:period] %>:</span>
84
+ <span class="text-muted"><%= red_t("red.mailers.digest.comparison.versus", period: @digest[:period]) %></span>
85
85
  </div>
86
86
  <div class="delta <%= delta_class %>">
87
- <%== arrow %> <%= delta > 0 ? "+#{delta}" : delta %> errors
87
+ <%== arrow %> <%= red_tp("red.mailers.digest.comparison.delta", count: delta.abs, delta: delta > 0 ? "+#{delta}" : delta) %>
88
88
  (<%= pct > 0 ? "+#{pct}" : pct %>%)
89
89
  </div>
90
90
  </div>
@@ -94,12 +94,12 @@
94
94
  <!-- Critical / High Unresolved -->
95
95
  <% if @digest[:critical_unresolved].any? %>
96
96
  <div class="card">
97
- <h3 style="margin: 0 0 12px 0; font-size: 16px;">Critical &amp; High Unresolved</h3>
97
+ <h3 style="margin: 0 0 12px 0; font-size: 16px;"><%= red_t("red.mailers.digest.critical_unresolved") %></h3>
98
98
  <table>
99
99
  <thead>
100
100
  <tr>
101
- <th>Severity</th>
102
- <th>Error</th>
101
+ <th><%= red_t("red.mailers.digest.columns.severity") %></th>
102
+ <th><%= red_t("red.mailers.digest.columns.error") %></th>
103
103
  <th></th>
104
104
  </tr>
105
105
  </thead>
@@ -107,14 +107,15 @@
107
107
  <% @digest[:critical_unresolved].each do |error| %>
108
108
  <tr>
109
109
  <td>
110
- <span class="badge badge-<%= error[:severity].to_s.downcase %>"><%= error[:severity].to_s.upcase %></span>
110
+ <%# Severity is a machine value; its label comes from red.common. %>
111
+ <span class="badge badge-<%= error[:severity].to_s.downcase %>"><%= red_t("red.common.severity.#{error[:severity].to_s.downcase}") %></span>
111
112
  </td>
112
113
  <td>
113
114
  <strong><%= error[:error_type] %></strong><br>
114
115
  <small class="text-muted"><%= error[:message] %></small>
115
116
  </td>
116
117
  <td>
117
- <a href="<%= @dashboard_url %>/errors/<%= error[:id] %>">View</a>
118
+ <a href="<%= @dashboard_url %>/errors/<%= error[:id] %>"><%= red_t("red.mailers.digest.view") %></a>
118
119
  </td>
119
120
  </tr>
120
121
  <% end %>
@@ -126,12 +127,12 @@
126
127
  <!-- Top Errors -->
127
128
  <% if @digest[:top_errors].any? %>
128
129
  <div class="card">
129
- <h3 style="margin: 0 0 12px 0; font-size: 16px;">Top Errors by Count</h3>
130
+ <h3 style="margin: 0 0 12px 0; font-size: 16px;"><%= red_t("red.mailers.digest.top_errors") %></h3>
130
131
  <table>
131
132
  <thead>
132
133
  <tr>
133
- <th>Error Type</th>
134
- <th>Count</th>
134
+ <th><%= red_t("red.mailers.digest.columns.error_type") %></th>
135
+ <th><%= red_t("red.mailers.digest.columns.count") %></th>
135
136
  <th></th>
136
137
  </tr>
137
138
  </thead>
@@ -145,7 +146,7 @@
145
146
  <td><%= error[:count] %></td>
146
147
  <td>
147
148
  <% if error[:id] %>
148
- <a href="<%= @dashboard_url %>/errors/<%= error[:id] %>">View</a>
149
+ <a href="<%= @dashboard_url %>/errors/<%= error[:id] %>"><%= red_t("red.mailers.digest.view") %></a>
149
150
  <% end %>
150
151
  </td>
151
152
  </tr>
@@ -158,14 +159,14 @@
158
159
  <!-- CTA -->
159
160
  <div class="card" style="text-align: center;">
160
161
  <a href="<%= @dashboard_url %>" style="display: inline-block; padding: 10px 24px; background: #3b82f6; color: #ffffff; border-radius: 6px; font-weight: 600; text-decoration: none;">
161
- Open Dashboard
162
+ <%= red_t("red.mailers.digest.open_dashboard") %>
162
163
  </a>
163
164
  </div>
164
165
 
165
166
  <!-- Footer -->
166
167
  <div class="footer">
167
- Automated digest from <strong>RED</strong> (Rails Error Dashboard)<br>
168
- <a href="<%= @dashboard_url %>/settings">Manage digest settings</a>
168
+ <%= red_t("red.mailers.digest.footer", product: red_t("red.mailers.shared.product_name")) %><br>
169
+ <a href="<%= @dashboard_url %>/settings"><%= red_t("red.mailers.digest.manage_settings") %></a>
169
170
  </div>
170
171
  </div>
171
172
  </body>
@@ -1,40 +1,51 @@
1
+ <%# Stat labels are NOT padded to a column. The English version aligned values %>
2
+ <%# at column 20 ("New Errors: 12"), which only works while every label %>
3
+ <%# is shorter than that. "Gesamtvorkommen" and friends are not. Label, space, %>
4
+ <%# value. (P4-T2 REQ-3) %>
1
5
  ==========================================
2
- RED Error Digest (<%= @digest[:period_label] %>)
6
+ <%= red_mail_t("red.mailers.digest.heading") %> (<%= @digest[:period_label] %>)
3
7
  ==========================================
4
- Generated: <%= @digest[:generated_at].strftime('%B %d, %Y at %I:%M %p %Z') %>
8
+ <%= red_mail_t("red.mailers.digest.generated_at") %>: <%= red_mail_time(@digest[:generated_at]) %>
5
9
 
6
10
  ------------------------------------------
7
- SUMMARY
11
+ <%= red_mail_t("red.mailers.digest.summary") %>
8
12
  ------------------------------------------
9
- New Errors: <%= @digest[:stats][:new_errors] %>
10
- Total Occurrences: <%= @digest[:stats][:total_occurrences] %>
11
- Resolved: <%= @digest[:stats][:resolved] %>
12
- Unresolved: <%= @digest[:stats][:unresolved] %>
13
- Critical/High: <%= @digest[:stats][:critical_high] %>
14
- Resolution Rate: <%= @digest[:stats][:resolution_rate] %>%
13
+ <%= red_mail_t("red.mailers.digest.stats.new_errors") %>: <%= @digest[:stats][:new_errors] %>
14
+ <%= red_mail_t("red.mailers.digest.stats.total_occurrences_text") %>: <%= @digest[:stats][:total_occurrences] %>
15
+ <%= red_mail_t("red.mailers.digest.stats.resolved") %>: <%= @digest[:stats][:resolved] %>
16
+ <%= red_mail_t("red.mailers.digest.stats.unresolved") %>: <%= @digest[:stats][:unresolved] %>
17
+ <%= red_mail_t("red.mailers.digest.stats.critical_high") %>: <%= @digest[:stats][:critical_high] %>
18
+ <%= red_mail_t("red.mailers.digest.stats.resolution_rate") %>: <%= @digest[:stats][:resolution_rate] %>%
15
19
 
16
20
  <% if @digest[:comparison][:error_delta_percentage] %>
17
21
  ------------------------------------------
18
- VS PREVIOUS PERIOD
19
- ------------------------------------------
20
- Current: <%= @digest[:comparison][:current_count] %> errors
21
- Previous: <%= @digest[:comparison][:previous_count] %> errors
22
- Change: <%= @digest[:comparison][:error_delta] > 0 ? "+#{@digest[:comparison][:error_delta]}" : @digest[:comparison][:error_delta] %> (<%= @digest[:comparison][:error_delta_percentage] > 0 ? "+#{@digest[:comparison][:error_delta_percentage]}" : @digest[:comparison][:error_delta_percentage] %>%)
22
+ <%= red_mail_t("red.mailers.digest.comparison.heading") %>
23
+ ------------------------------------------
24
+ <%
25
+ delta = @digest[:comparison][:error_delta]
26
+ pct = @digest[:comparison][:error_delta_percentage]
27
+ signed_delta = delta > 0 ? "+#{delta}" : delta.to_s
28
+ signed_pct = pct > 0 ? "+#{pct}" : pct.to_s
29
+ %>
30
+ <%= red_mail_t("red.mailers.digest.comparison.current") %>: <%= red_mail_tp("red.mailers.digest.comparison.delta", count: @digest[:comparison][:current_count], delta: @digest[:comparison][:current_count]) %>
31
+ <%= red_mail_t("red.mailers.digest.comparison.previous") %>: <%= red_mail_tp("red.mailers.digest.comparison.delta", count: @digest[:comparison][:previous_count], delta: @digest[:comparison][:previous_count]) %>
32
+ <%= red_mail_t("red.mailers.digest.comparison.change") %>: <%= signed_delta %> (<%= signed_pct %>%)
23
33
  <% end %>
24
34
 
25
35
  <% if @digest[:critical_unresolved].any? %>
26
36
  ------------------------------------------
27
- CRITICAL / HIGH UNRESOLVED
37
+ <%= red_mail_t("red.mailers.digest.critical_unresolved") %>
28
38
  ------------------------------------------
29
39
  <% @digest[:critical_unresolved].each do |error| %>
30
- [<%= error[:severity].to_s.upcase %>] <%= error[:error_type] %>: <%= error[:message] %>
40
+ <%# Severity is a machine value; its display label comes from red.common. %>
41
+ [<%= red_mail_t("red.common.severity.#{error[:severity].to_s.downcase}") %>] <%= error[:error_type] %>: <%= error[:message] %>
31
42
  → <%= @dashboard_url %>/errors/<%= error[:id] %>
32
43
  <% end %>
33
44
  <% end %>
34
45
 
35
46
  <% if @digest[:top_errors].any? %>
36
47
  ------------------------------------------
37
- TOP ERRORS BY COUNT
48
+ <%= red_mail_t("red.mailers.digest.top_errors") %>
38
49
  ------------------------------------------
39
50
  <% @digest[:top_errors].each_with_index do |error, i| %>
40
51
  <%= i + 1 %>. <%= error[:error_type] %> (<%= error[:count] %>x): <%= error[:message] %>
@@ -44,6 +55,6 @@ TOP ERRORS BY COUNT
44
55
 
45
56
  ------------------------------------------
46
57
 
47
- View the full dashboard:
58
+ <%= red_mail_t("red.mailers.digest.view_dashboard_text") %>
48
59
  <%= @dashboard_url %>
49
- This is an automated digest from RED (Rails Error Dashboard).
60
+ <%= red_mail_t("red.mailers.digest.footer", product: red_mail_t("red.mailers.shared.product_name")) %>
@@ -128,19 +128,19 @@
128
128
  </head>
129
129
  <body>
130
130
  <div class="header">
131
- <h1>🚨 Error Alert</h1>
131
+ <h1><%= red_t("red.mailers.error_alert.heading") %></h1>
132
132
  </div>
133
133
 
134
134
  <div class="content">
135
135
  <div class="info-grid">
136
- <div class="label">Application:</div>
137
- <div class="value"><strong><%= @error_log.application&.name || 'Unknown' %></strong></div>
136
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.application") %>:</div>
137
+ <div class="value"><strong><%= @error_log.application&.name || red_t("red.mailers.shared.unknown_application") %></strong></div>
138
138
 
139
- <div class="label">Error Type:</div>
139
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.error_type") %>:</div>
140
140
  <div class="value"><code><%= @error_log.error_type %></code></div>
141
141
 
142
142
  <% if @error_log.platform.present? %>
143
- <div class="label">Platform:</div>
143
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.platform") %>:</div>
144
144
  <div class="value">
145
145
  <span class="badge badge-<%= @error_log.platform.downcase %>">
146
146
  <%= @error_log.platform %>
@@ -148,47 +148,49 @@
148
148
  </div>
149
149
  <% end %>
150
150
 
151
- <div class="label">Occurred At:</div>
152
- <div class="value"><%= @error_log.occurred_at.strftime('%B %d, %Y at %I:%M %p %Z') %></div>
151
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.occurred_at") %>:</div>
152
+ <div class="value"><%= red_mail_time(@error_log.occurred_at) %></div>
153
153
 
154
154
  <% if @error_log.user_id.present? %>
155
- <div class="label">User ID:</div>
155
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.user_id") %>:</div>
156
156
  <div class="value"><%= @error_log.user_id %></div>
157
157
  <% end %>
158
158
 
159
159
  <% if @error_log.ip_address.present? %>
160
- <div class="label">IP Address:</div>
160
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.ip_address") %>:</div>
161
161
  <div class="value"><%= @error_log.ip_address %></div>
162
162
  <% end %>
163
163
 
164
164
  <% if @error_log.request_url.present? %>
165
- <div class="label">Request URL:</div>
165
+ <div class="label"><%= red_t("red.mailers.error_alert.labels.request_url") %>:</div>
166
166
  <div class="value"><%= @error_log.request_url %></div>
167
167
  <% end %>
168
168
  </div>
169
169
 
170
170
  <div class="error-message">
171
- <strong>Message:</strong><br/>
171
+ <strong><%= red_t("red.mailers.error_alert.labels.message") %>:</strong><br/>
172
172
  <code><%= @error_log.message %></code>
173
173
  </div>
174
174
 
175
175
  <% if @error_log.backtrace.present? %>
176
+ <% backtrace_lines = @error_log.backtrace.split("\n").first(10) %>
176
177
  <div class="backtrace">
177
- <strong>Stack Trace (first 10 lines):</strong>
178
- <pre><%= @error_log.backtrace.split("\n").first(10).join("\n") %></pre>
178
+ <strong><%= red_tp("red.mailers.error_alert.stack_trace", count: backtrace_lines.length) %>:</strong>
179
+ <%# Backtrace content is diagnostic output and is never translated. %>
180
+ <pre><%= backtrace_lines.join("\n") %></pre>
179
181
  </div>
180
182
  <% end %>
181
183
 
182
184
  <div style="text-align: center;">
183
185
  <a href="<%= @dashboard_url %>" class="button">
184
- View Full Details in Dashboard
186
+ <%= red_t("red.mailers.error_alert.view_details") %>
185
187
  </a>
186
188
  </div>
187
189
 
188
190
  <div class="footer">
189
191
  <p>
190
- This is an automated error notification from Rails Error Dashboard.<br/>
191
- Error ID: <%= @error_log.id %>
192
+ <%= red_t("red.mailers.error_alert.footer", product: red_t("red.mailers.shared.product_name")) %><br/>
193
+ <%= red_t("red.mailers.error_alert.labels.error_id") %>: <%= @error_log.id %>
192
194
  </p>
193
195
  </div>
194
196
  </div>
@@ -1,32 +1,39 @@
1
+ <%# Separators are fixed-width but LABELS ARE NOT ALIGNED TO A COLUMN. %>
2
+ <%# Padding "Application:" to a column assumes every translation is about as %>
3
+ <%# long as the English one; German runs ~30% longer and would push values %>
4
+ <%# past 80 characters or force translators to abbreviate. Label, space, %>
5
+ <%# value — nothing to break. (P4-T2 REQ-3) %>
1
6
  ==========================================
2
- 🚨 ERROR ALERT
7
+ <%= red_mail_t("red.mailers.error_alert.heading") %>
3
8
  ==========================================
4
9
 
5
- Application: <%= @error_log.application&.name || 'Unknown' %>
6
- Error Type: <%= @error_log.error_type %>
7
- <% if @error_log.platform.present? %>Platform: <%= @error_log.platform %><% end %>
8
- Occurred At: <%= @error_log.occurred_at.strftime('%B %d, %Y at %I:%M %p %Z') %>
9
- <% if @error_log.user_id.present? %>User ID: <%= @error_log.user_id %><% end %>
10
- <% if @error_log.ip_address.present? %>IP Address: <%= @error_log.ip_address %><% end %>
11
- <% if @error_log.request_url.present? %>Request URL: <%= @error_log.request_url %><% end %>
10
+ <%= red_mail_t("red.mailers.error_alert.labels.application") %>: <%= @error_log.application&.name || red_mail_t("red.mailers.shared.unknown_application") %>
11
+ <%= red_mail_t("red.mailers.error_alert.labels.error_type") %>: <%= @error_log.error_type %>
12
+ <% if @error_log.platform.present? %><%= red_mail_t("red.mailers.error_alert.labels.platform") %>: <%= @error_log.platform %><% end %>
13
+ <%= red_mail_t("red.mailers.error_alert.labels.occurred_at") %>: <%= red_mail_time(@error_log.occurred_at) %>
14
+ <% if @error_log.user_id.present? %><%= red_mail_t("red.mailers.error_alert.labels.user_id") %>: <%= @error_log.user_id %><% end %>
15
+ <% if @error_log.ip_address.present? %><%= red_mail_t("red.mailers.error_alert.labels.ip_address") %>: <%= @error_log.ip_address %><% end %>
16
+ <% if @error_log.request_url.present? %><%= red_mail_t("red.mailers.error_alert.labels.request_url") %>: <%= @error_log.request_url %><% end %>
12
17
 
13
18
  ------------------------------------------
14
- MESSAGE
19
+ <%= red_mail_t("red.mailers.error_alert.labels.message") %>
15
20
  ------------------------------------------
21
+ <%# Error content is diagnostic output and is never translated. %>
16
22
  <%= @error_log.message %>
17
23
 
18
24
  <% if @error_log.backtrace.present? %>
25
+ <% backtrace_lines = @error_log.backtrace.split("\n").first(10) %>
19
26
  ------------------------------------------
20
- STACK TRACE (first 10 lines)
27
+ <%= red_mail_tp("red.mailers.error_alert.stack_trace", count: backtrace_lines.length) %>
21
28
  ------------------------------------------
22
- <%= @error_log.backtrace.split("\n").first(10).join("\n") %>
29
+ <%= backtrace_lines.join("\n") %>
23
30
  <% end %>
24
31
 
25
32
  ------------------------------------------
26
33
 
27
- View full details in the dashboard:
34
+ <%= red_mail_t("red.mailers.error_alert.view_details_text") %>
28
35
  <%= @dashboard_url %>
29
36
 
30
- Error ID: <%= @error_log.id %>
37
+ <%= red_mail_t("red.mailers.error_alert.labels.error_id") %>: <%= @error_log.id %>
31
38
 
32
- This is an automated error notification from Rails Error Dashboard.
39
+ <%= red_mail_t("red.mailers.error_alert.footer", product: red_mail_t("red.mailers.shared.product_name")) %>
@@ -5,31 +5,35 @@
5
5
  <div>
6
6
  <div id="red-ai-help-title" class="red-ai-help-title">
7
7
  <i class="bi bi-stars"></i>
8
- AI Help
8
+ <%= red_t("red.errors.ai_help.title") %>
9
9
  </div>
10
10
  <div class="red-ai-help-subtitle">
11
- <%= RailsErrorDashboard.configuration.effective_llm_provider.to_s.titleize %>
11
+ <%# Brand name, identical in every locale, but looked up rather than
12
+ titleized: "openai".titleize is "Openai", which is not how the
13
+ company writes it. An unknown provider falls back to its raw value. %>
14
+ <% llm_provider = RailsErrorDashboard.configuration.effective_llm_provider %>
15
+ <%= red_t("red.errors.ai_help.providers.#{llm_provider}", default: llm_provider.to_s) %>
12
16
  &middot;
13
17
  <%= RailsErrorDashboard.configuration.effective_llm_model %>
14
18
  </div>
15
19
  </div>
16
- <button type="button" class="btn-close" data-red-action="close-ai-help" aria-label="Close AI Help"></button>
20
+ <button type="button" class="btn-close" data-red-action="close-ai-help" aria-label="<%= red_t("red.errors.ai_help.close") %>"></button>
17
21
  </div>
18
22
 
19
23
  <div id="red-ai-help-messages" class="red-ai-help-messages" aria-live="polite">
20
24
  <div class="red-ai-help-empty">
21
- Ask about root cause, reproduction steps, likely fixes, or what to inspect next.
25
+ <%= red_t("red.errors.ai_help.empty") %>
22
26
  </div>
23
27
  </div>
24
28
 
25
29
  <form id="red-ai-help-form" class="red-ai-help-form" data-ai-help-url="<%= ai_help_error_path(error, **app_context) %>">
26
- <label for="red-ai-help-question">Question</label>
27
- <textarea id="red-ai-help-question" name="question" rows="4" maxlength="4000" placeholder="What is the likely root cause?"></textarea>
30
+ <label for="red-ai-help-question"><%= red_t("red.errors.ai_help.question_label") %></label>
31
+ <textarea id="red-ai-help-question" name="question" rows="4" maxlength="4000" placeholder="<%= red_t("red.errors.ai_help.question_placeholder") %>"></textarea>
28
32
  <div class="red-ai-help-footer">
29
33
  <span id="red-ai-help-status" class="red-ai-help-status"></span>
30
34
  <button type="submit" class="btn btn-primary">
31
35
  <i class="bi bi-send"></i>
32
- Ask
36
+ <%= red_t("red.errors.ai_help.ask") %>
33
37
  </button>
34
38
  </div>
35
39
  </form>