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
@@ -1,28 +1,31 @@
1
- <% cache [error, 'error_details_v1'] do %>
1
+ <%# The locale is part of the cache key: this fragment now contains
2
+ translated text, and without it the first locale to render an error
3
+ would be served to every other one. %>
4
+ <% cache [error, 'error_details_v2', red_locale] do %>
2
5
  <div class="card mb-4" id="section-error-info">
3
6
  <div class="card-header bg-danger text-white">
4
7
  <div class="d-flex justify-content-between align-items-center">
5
8
  <h5 class="mb-0">
6
9
  <i class="bi bi-bug-fill"></i> <%= error.error_type %>
7
10
  <% if error.recent? %>
8
- <span class="badge bg-success ms-2" data-bs-toggle="tooltip" title="Error occurred within the last hour">NEW</span>
11
+ <span class="badge bg-success ms-2" data-bs-toggle="tooltip" title="<%= red_t("red.errors.info.new_tooltip") %>"><%= red_t("red.errors.info.new_badge") %></span>
9
12
  <% end %>
10
13
  <% if error.reopened? %>
11
- <span class="badge bg-warning text-dark ms-2" data-bs-toggle="tooltip" title="Previously resolved, recurred <%= error.reopened_at&.strftime('%b %d, %Y %H:%M') %>">
12
- <i class="bi bi-arrow-counterclockwise"></i> Reopened
14
+ <span class="badge bg-warning text-dark ms-2" data-bs-toggle="tooltip" title="<%= red_t("red.errors.info.reopened_tooltip", time: error.reopened_at&.strftime(red_time_format(:datetime))) %>">
15
+ <i class="bi bi-arrow-counterclockwise"></i> <%= red_t("red.errors.info.reopened") %>
13
16
  </span>
14
17
  <% end %>
15
18
  </h5>
16
- <button type="button" class="btn btn-sm btn-outline-light" data-red-action="copy" data-red-copy-text="<%= j error.error_type %>" title="Copy error type">
19
+ <button type="button" class="btn btn-sm btn-outline-light" data-red-action="copy" data-red-copy-text="<%= j error.error_type %>" title="<%= red_t("red.errors.info.copy_error_type") %>">
17
20
  <i class="bi bi-clipboard"></i>
18
21
  </button>
19
22
  </div>
20
23
  </div>
21
24
  <div class="card-body">
22
25
  <div class="d-flex justify-content-between align-items-center mb-3">
23
- <h6 class="text-muted mb-0">Error Message:</h6>
24
- <button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.message %>" title="Copy error message">
25
- <i class="bi bi-clipboard"></i> Copy
26
+ <h6 class="text-muted mb-0"><%= red_t("red.errors.info.message_label") %></h6>
27
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.message %>" title="<%= red_t("red.errors.info.copy_message") %>">
28
+ <i class="bi bi-clipboard"></i> <%= red_t("red.errors.info.copy") %>
26
29
  </button>
27
30
  </div>
28
31
  <div class="alert alert-danger">
@@ -35,7 +38,7 @@
35
38
  <% if cause_chain.is_a?(Array) && cause_chain.any? %>
36
39
  <div class="mt-4 mb-3">
37
40
  <h6 class="text-muted mb-2">
38
- <i class="bi bi-link-45deg"></i> Exception Cause Chain
41
+ <i class="bi bi-link-45deg"></i> <%= red_t("red.errors.info.cause_chain.title") %>
39
42
  <span class="badge bg-secondary ms-1"><%= cause_chain.length %></span>
40
43
  </h6>
41
44
  <div class="cause-chain">
@@ -46,7 +49,7 @@
46
49
  <div class="card-header bg-warning bg-opacity-10 py-2">
47
50
  <div class="d-flex justify-content-between align-items-center">
48
51
  <div>
49
- <small class="text-muted">Caused by<%= " (##{index + 1})" if cause_chain.length > 1 %>:</small>
52
+ <small class="text-muted"><%= cause_chain.length > 1 ? red_t("red.errors.info.cause_chain.caused_by_indexed", index: index + 1) : red_t("red.errors.info.cause_chain.caused_by") %></small>
50
53
  <strong class="ms-1"><code><%= cause["class_name"] %></code></strong>
51
54
  </div>
52
55
  <% if cause_bt.any? %>
@@ -54,7 +57,7 @@
54
57
  data-bs-toggle="collapse"
55
58
  data-bs-target="#cause-backtrace-<%= index %>"
56
59
  aria-expanded="false">
57
- <small><i class="bi bi-code-slash"></i> Backtrace</small>
60
+ <small><i class="bi bi-code-slash"></i> <%= red_t("red.errors.info.cause_chain.backtrace") %></small>
58
61
  </button>
59
62
  <% end %>
60
63
  </div>
@@ -84,19 +87,19 @@
84
87
 
85
88
  <div class="d-flex justify-content-between align-items-center mb-2 mt-4">
86
89
  <h6 class="text-muted mb-0">
87
- Backtrace:
90
+ <%= red_t("red.errors.info.backtrace.label") %>
88
91
  <% if error.backtrace.present? %>
89
92
  <% frames = parse_backtrace(error.backtrace) %>
90
93
  <% app_frames = filter_app_code(frames) %>
91
94
  <% framework_frames = filter_framework_code(frames) %>
92
95
  <small class="text-muted">
93
- (<%= app_frames.count %> your code, <%= framework_frames.count %> framework/gems)
96
+ <%= red_t("red.errors.info.backtrace.frame_counts", app: app_frames.count, framework: framework_frames.count) %>
94
97
  </small>
95
98
  <% end %>
96
99
  </h6>
97
100
  <% if error.backtrace.present? %>
98
- <button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.backtrace %>" title="Copy full backtrace">
99
- <i class="bi bi-clipboard"></i> Copy Full Backtrace
101
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.backtrace %>" title="<%= red_t("red.errors.info.backtrace.copy_full_title") %>">
102
+ <i class="bi bi-clipboard"></i> <%= red_t("red.errors.info.backtrace.copy_full") %>
100
103
  </button>
101
104
  <% end %>
102
105
  </div>
@@ -111,9 +114,9 @@
111
114
  <div class="card-header bg-success bg-opacity-10 border-success">
112
115
  <div class="d-flex justify-content-between align-items-center">
113
116
  <strong class="text-success">
114
- <i class="bi bi-code-square"></i> Your Code
117
+ <i class="bi bi-code-square"></i> <%= red_t("red.errors.info.backtrace.your_code") %>
115
118
  </strong>
116
- <span class="badge bg-success"><%= app_frames.count %> frames</span>
119
+ <span class="badge bg-success"><%= red_tp("red.errors.info.backtrace.frames", count: app_frames.count) %></span>
117
120
  </div>
118
121
  </div>
119
122
  <div class="card-body p-0">
@@ -134,7 +137,7 @@
134
137
  data-bs-target="#source-code-<%= index %>"
135
138
  aria-expanded="false"
136
139
  aria-controls="source-code-<%= index %>">
137
- <i class="bi bi-code-slash"></i> <span class="d-none d-md-inline">View Source</span>
140
+ <i class="bi bi-code-slash"></i> <span class="d-none d-md-inline"><%= red_t("red.errors.info.backtrace.view_source") %></span>
138
141
  </button>
139
142
  <% end %>
140
143
  </div>
@@ -159,9 +162,9 @@
159
162
  <h2 class="accordion-header">
160
163
  <button class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#frameworkBacktrace" aria-expanded="false" aria-controls="frameworkBacktrace">
161
164
  <i class="bi bi-gear me-2 text-warning"></i>
162
- <strong>Framework & Gem Code</strong>
163
- <span class="badge bg-secondary ms-2"><%= framework_frames.count %> frames</span>
164
- <small class="text-muted ms-2">(click to expand)</small>
165
+ <strong><%= red_t("red.errors.info.backtrace.framework_code") %></strong>
166
+ <span class="badge bg-secondary ms-2"><%= red_tp("red.errors.info.backtrace.frames", count: framework_frames.count) %></span>
167
+ <small class="text-muted ms-2"><%= red_t("red.errors.info.backtrace.click_to_expand") %></small>
165
168
  </button>
166
169
  </h2>
167
170
  <div id="frameworkBacktrace" class="accordion-collapse collapse" data-bs-parent="#frameworkBacktraceAccordion">
@@ -183,7 +186,7 @@
183
186
  </div>
184
187
  <% end %>
185
188
  <% else %>
186
- <p class="text-muted">No backtrace available</p>
189
+ <p class="text-muted"><%= red_t("red.errors.info.backtrace.none") %></p>
187
190
  <% end %>
188
191
  </div>
189
192
  </div>
@@ -9,13 +9,13 @@
9
9
  <div style="display: flex; align-items: center; gap: 8px;">
10
10
  <span class="badge bg-<%= severity_color(error.severity) %>" style="display: inline-flex; align-items: center; gap: 4px;">
11
11
  <span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
12
- <%= error.severity %>
12
+ <%= red_t("red.common.severity.#{error.severity}", default: error.severity.to_s) %>
13
13
  </span>
14
14
  <div style="min-width: 0;">
15
15
  <div style="font-weight: 600; color: var(--text-primary); font-family: var(--font-mono); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
16
16
  <%= error.error_type %>
17
17
  <% if error.recent? %>
18
- <span style="display: inline-block; padding: 1px 6px; font-size: 9px; font-weight: 600; border-radius: var(--radius-full); background: var(--status-success-bg); color: var(--status-success); margin-left: 4px; text-transform: uppercase;">NEW</span>
18
+ <span style="display: inline-block; padding: 1px 6px; font-size: 9px; font-weight: 600; border-radius: var(--radius-full); background: var(--status-success-bg); color: var(--status-success); margin-left: 4px; text-transform: uppercase;"><%= red_t("red.errors.row.new_badge") %></span>
19
19
  <% end %>
20
20
  </div>
21
21
  <div style="color: var(--text-tertiary); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<%= error.message %>"><%= error.message %></div>
@@ -24,36 +24,43 @@
24
24
  </td>
25
25
  <td style="padding: var(--space-3) var(--space-4);">
26
26
  <%
27
- # Use the model's status field if it has a meaningful workflow status
27
+ # Use the model's status field if it has a meaningful workflow status.
28
+ #
29
+ # status_key stays the machine value ("in_progress"): it drives the colour
30
+ # lookup and the translation key, neither of which may vary by locale.
31
+ # Only the rendered label is translated. The old code displayed the value
32
+ # itself via tr('_', ' ') plus a CSS capitalize, which is English
33
+ # morphology applied to an identifier.
28
34
  raw_status = error.respond_to?(:status) ? error.status : nil
29
- status_text = if error.respond_to?(:muted?) && error.muted?
35
+ status_key = if error.respond_to?(:muted?) && error.muted?
30
36
  'muted'
31
37
  elsif error.respond_to?(:snoozed?) && error.snoozed?
32
38
  'snoozed'
33
39
  elsif raw_status.present? && raw_status != 'new'
34
- raw_status.tr('_', ' ')
40
+ raw_status
35
41
  elsif error.resolved?
36
42
  'resolved'
37
43
  else
38
44
  'unresolved'
39
45
  end
46
+ status_text = red_t("red.errors.row.status.#{status_key}", default: status_key.tr('_', ' '))
40
47
  status_colors = {
41
48
  'unresolved' => { bg: 'var(--status-critical-bg)', color: 'var(--status-critical)' },
42
49
  'resolved' => { bg: 'var(--status-success-bg)', color: 'var(--status-success)' },
43
- 'in progress' => { bg: 'var(--status-info-bg)', color: 'var(--status-info)' },
50
+ 'in_progress' => { bg: 'var(--status-info-bg)', color: 'var(--status-info)' },
44
51
  'investigating' => { bg: 'var(--status-caution-bg)', color: 'var(--status-caution)' },
45
- 'wont fix' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
52
+ 'wont_fix' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
46
53
  'assigned' => { bg: 'var(--accent-subtle)', color: 'var(--accent)' },
47
54
  'snoozed' => { bg: 'var(--status-caution-bg)', color: 'var(--status-caution)' },
48
55
  'muted' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
49
56
  }
50
- sc = status_colors[status_text] || status_colors['unresolved']
57
+ sc = status_colors[status_key] || status_colors['unresolved']
51
58
  %>
52
59
  <span style="display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; font-size: 11px; font-weight: 600; border-radius: var(--radius-full); background: <%= sc[:bg] %>; color: <%= sc[:color] %>; text-transform: capitalize;">
53
60
  <%= status_text %>
54
61
  </span>
55
62
  <% if error.reopened? %>
56
- <i class="bi bi-arrow-counterclockwise" style="color: var(--status-warning); margin-left: 4px; font-size: 12px;" title="Reopened"></i>
63
+ <i class="bi bi-arrow-counterclockwise" style="color: var(--status-warning); margin-left: 4px; font-size: 12px;" title="<%= red_t("red.errors.row.reopened") %>"></i>
57
64
  <% end %>
58
65
  </td>
59
66
  <td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);">
@@ -61,7 +68,7 @@
61
68
  </td>
62
69
  <td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);">
63
70
  <% if error.user_id %>
64
- <%= link_to error.user_id, errors_path(user_id: error.user_id, unresolved: '0'), style: "color: inherit; text-decoration: none;", title: "View all errors for user #{error.user_id}" %>
71
+ <%= link_to error.user_id, errors_path(user_id: error.user_id, unresolved: '0'), style: "color: inherit; text-decoration: none;", title: red_t("red.errors.row.view_user_errors", user: error.user_id) %>
65
72
  <% else %>
66
73
  <span style="color: var(--text-tertiary);">&mdash;</span>
67
74
  <% end %>
@@ -70,8 +77,13 @@
70
77
  <%= local_time_ago(error.last_seen_at) %>
71
78
  </td>
72
79
  <% if local_assigns[:show_application] %>
73
- <td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.application&.name || 'Unknown' %>">
74
- <span class="badge bg-info"><%= error.application&.name || 'Unknown' %></span>
80
+ <%
81
+ # An application name is user data and is never translated; only the
82
+ # placeholder for a missing one is.
83
+ application_name = error.application&.name.presence || red_t("red.common.unknown")
84
+ %>
85
+ <td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= application_name %>">
86
+ <span class="badge bg-info"><%= application_name %></span>
75
87
  </td>
76
88
  <% end %>
77
89
  <% if local_assigns[:show_platform] %>
@@ -7,17 +7,18 @@
7
7
  <div class="card mb-4" id="section-instance-variables">
8
8
  <div class="card-header">
9
9
  <h5 class="mb-0">
10
- <i class="bi bi-box"></i> Instance Variables
11
- <span class="badge bg-secondary"><%= instance_vars.size %> captured</span>
10
+ <i class="bi bi-box"></i> <%= red_t("red.errors.variables.instance_title") %>
11
+ <span class="badge bg-secondary"><%= red_tp("red.errors.variables.captured", count: instance_vars.size) %></span>
12
12
  </h5>
13
13
  <small class="text-muted">
14
- Instance variables from
14
+ <%# Whole sentences per branch: the original spliced a <code>
15
+ element into the middle of one, which does not survive
16
+ reordering into every language. %>
15
17
  <% if self_class && self_class["value"] %>
16
- <code><%= self_class["value"] %></code>
18
+ <%= red_t("red.errors.variables.instance_hint_html", klass: tag.code(self_class["value"])) %>
17
19
  <% else %>
18
- the object
20
+ <%= red_t("red.errors.variables.instance_hint_generic") %>
19
21
  <% end %>
20
- where the exception was raised
21
22
  </small>
22
23
  </div>
23
24
  <div class="card-body p-0">
@@ -25,9 +26,9 @@
25
26
  <table class="table table-sm table-hover mb-0">
26
27
  <thead class="table-light">
27
28
  <tr>
28
- <th width="200">Variable</th>
29
- <th width="120">Type</th>
30
- <th>Value</th>
29
+ <th width="200"><%= red_t("red.errors.variables.column_variable") %></th>
30
+ <th width="120"><%= red_t("red.errors.variables.column_type") %></th>
31
+ <th><%= red_t("red.errors.variables.column_value") %></th>
31
32
  </tr>
32
33
  </thead>
33
34
  <tbody>
@@ -37,11 +38,11 @@
37
38
  <td><small class="text-muted"><%= info["type"] %></small></td>
38
39
  <td>
39
40
  <% if info["filtered"] %>
40
- <span class="badge bg-warning text-dark">[FILTERED]</span>
41
+ <span class="badge bg-warning text-dark"><%= red_t("red.errors.variables.filtered") %></span>
41
42
  <% else %>
42
43
  <code style="word-break: break-all; font-size: 0.85em;"><%= truncate(info["value"].to_s, length: 500) %></code>
43
44
  <% if info["truncated"] %>
44
- <span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;">truncated</span>
45
+ <span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;"><%= red_t("red.errors.variables.truncated") %></span>
45
46
  <% end %>
46
47
  <% end %>
47
48
  </td>
@@ -3,11 +3,11 @@
3
3
  <div class="card-header d-flex justify-content-between align-items-center">
4
4
  <h5 class="mb-0">
5
5
  <i class="bi bi-link-45deg me-2"></i>
6
- Issue Tracker
6
+ <%= red_t("red.errors.issue_section.title") %>
7
7
  </h5>
8
8
  <% if @error.external_issue_url.present? %>
9
9
  <a href="<%= @error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
10
- <i class="bi bi-box-arrow-up-right me-1"></i> View Issue
10
+ <i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.issue_section.view_issue") %>
11
11
  </a>
12
12
  <% end %>
13
13
  </div>
@@ -27,14 +27,16 @@
27
27
  <div class="d-flex align-items-center gap-2 mb-3">
28
28
  <span class="badge bg-success">
29
29
  <i class="bi <%= icon %> me-1"></i>
30
- <%= provider&.capitalize || "Linked" %> #<%= @error.external_issue_number || "?" %>
30
+ <%# Provider names are brands: github -> "GitHub", not "Github". %>
31
+ <% issue_provider = provider.present? ? red_t("red.errors.providers.#{provider}", default: provider.to_s) : red_t("red.errors.issue_section.linked") %>
32
+ <%= red_t("red.errors.issue_section.issue_reference", provider: issue_provider, number: @error.external_issue_number || "?") %>
31
33
  </span>
32
34
  <% if issue %>
33
35
  <% state_color = issue[:state] == "open" ? "success" : "secondary" %>
34
36
  <% state_icon = issue[:state] == "open" ? "bi-circle-fill" : "bi-check-circle-fill" %>
35
37
  <span class="badge bg-<%= state_color %>">
36
38
  <i class="bi <%= state_icon %> me-1" style="font-size: 0.6em;"></i>
37
- <%= issue[:state]&.capitalize %>
39
+ <%= red_t("red.errors.issue_section.states.#{issue[:state]}", default: issue[:state].to_s) %>
38
40
  </span>
39
41
  <% end %>
40
42
  </div>
@@ -43,7 +45,7 @@
43
45
  <div class="row">
44
46
  <!-- Assignees -->
45
47
  <div class="col-md-6 mb-2">
46
- <small class="text-muted d-block mb-1">Assignees</small>
48
+ <small class="text-muted d-block mb-1"><%= red_t("red.errors.issue_section.assignees") %></small>
47
49
  <% if issue[:assignees].present? && issue[:assignees].any? %>
48
50
  <div class="d-flex align-items-center gap-2 flex-wrap">
49
51
  <% issue[:assignees].each do |assignee| %>
@@ -56,13 +58,13 @@
56
58
  <% end %>
57
59
  </div>
58
60
  <% else %>
59
- <span class="text-muted" style="font-size: 0.85em;">Unassigned</span>
61
+ <span class="text-muted" style="font-size: 0.85em;"><%= red_t("red.errors.issue_section.unassigned") %></span>
60
62
  <% end %>
61
63
  </div>
62
64
 
63
65
  <!-- Labels -->
64
66
  <div class="col-md-6 mb-2">
65
- <small class="text-muted d-block mb-1">Labels</small>
67
+ <small class="text-muted d-block mb-1"><%= red_t("red.errors.issue_section.labels") %></small>
66
68
  <% if issue[:labels].present? && issue[:labels].any? %>
67
69
  <div class="d-flex align-items-center gap-1 flex-wrap">
68
70
  <% issue[:labels].each do |label| %>
@@ -74,7 +76,7 @@
74
76
  <% end %>
75
77
  </div>
76
78
  <% else %>
77
- <span class="text-muted" style="font-size: 0.85em;">No labels</span>
79
+ <span class="text-muted" style="font-size: 0.85em;"><%= red_t("red.errors.issue_section.no_labels") %></span>
78
80
  <% end %>
79
81
  </div>
80
82
  </div>
@@ -84,26 +86,30 @@
84
86
  <!-- No linked issue — show create/link options -->
85
87
  <div class="row">
86
88
  <div class="col-md-6">
87
- <h6 class="text-muted mb-2">Create New Issue</h6>
89
+ <h6 class="text-muted mb-2"><%= red_t("red.errors.issue_section.create_new") %></h6>
88
90
  <% provider = RailsErrorDashboard.configuration.effective_issue_tracker_provider %>
89
91
  <% if provider && RailsErrorDashboard.configuration.effective_issue_tracker_token %>
90
92
  <%= button_to create_issue_error_path(@error), method: :post, class: "btn btn-sm btn-primary" do %>
91
93
  <i class="bi bi-plus-circle me-1"></i>
92
- Create <%= provider.to_s.capitalize %> Issue
94
+ <%= red_t("red.errors.issue_section.create_issue", provider: red_t("red.errors.providers.#{provider}", default: provider.to_s)) %>
93
95
  <% end %>
94
96
  <% else %>
95
97
  <p class="text-muted small mb-0">
96
98
  <i class="bi bi-info-circle"></i>
97
- Configure <code>issue_tracker_token</code> and <code>git_repository_url</code> in your initializer to enable issue creation.
99
+ <%# The config option names are identifiers and stay English;
100
+ only the sentence around them is translated. %>
101
+ <%= red_t("red.errors.issue_section.configure_html",
102
+ token: tag.code("issue_tracker_token"),
103
+ url: tag.code("git_repository_url")) %>
98
104
  </p>
99
105
  <% end %>
100
106
  </div>
101
107
  <div class="col-md-6">
102
- <h6 class="text-muted mb-2">Link Existing Issue</h6>
108
+ <h6 class="text-muted mb-2"><%= red_t("red.errors.issue_section.link_existing") %></h6>
103
109
  <%= form_tag link_issue_error_path(@error), method: :post, class: "d-flex gap-2" do %>
104
110
  <%= text_field_tag :issue_url, nil, placeholder: "https://github.com/user/repo/issues/42", class: "form-control form-control-sm", required: true %>
105
111
  <button type="submit" class="btn btn-sm btn-outline-secondary text-nowrap">
106
- <i class="bi bi-link"></i> Link
112
+ <i class="bi bi-link"></i> <%= red_t("red.errors.issue_section.link") %>
107
113
  </button>
108
114
  <% end %>
109
115
  </div>
@@ -8,20 +8,20 @@
8
8
  <div class="card mb-4" id="section-llm-summary">
9
9
  <div class="card-header">
10
10
  <h5 class="mb-0">
11
- <i class="bi bi-robot text-info" aria-hidden="true"></i> LLM Calls
11
+ <i class="bi bi-robot text-info" aria-hidden="true"></i> <%= red_t("red.errors.llm_summary.title") %>
12
12
  <span class="badge bg-info text-dark"><%= llm_summary[:total_calls] %></span>
13
13
  </h5>
14
- <small class="text-muted">Aggregated from request breadcrumbs</small>
14
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.hint") %></small>
15
15
  </div>
16
16
  <div class="card-body">
17
17
  <div class="row text-center mb-3">
18
18
  <div class="col-4">
19
19
  <div class="fs-4 fw-bold"><%= llm_summary[:total_calls] %></div>
20
- <small class="text-muted">Calls</small>
20
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.calls") %></small>
21
21
  </div>
22
22
  <div class="col-4">
23
23
  <div class="fs-4 fw-bold"><%= number_with_delimiter(llm_summary[:total_tokens]) %></div>
24
- <small class="text-muted">Tokens</small>
24
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.tokens") %></small>
25
25
  </div>
26
26
  <div class="col-4">
27
27
  <div class="fs-4 fw-bold">
@@ -31,32 +31,32 @@
31
31
  <span class="text-muted">--</span>
32
32
  <% end %>
33
33
  </div>
34
- <small class="text-muted">Cost</small>
34
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.cost") %></small>
35
35
  </div>
36
36
  </div>
37
37
 
38
38
  <div class="mb-3">
39
39
  <div class="d-flex justify-content-between mb-1">
40
- <small class="text-muted">Input</small>
40
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.input") %></small>
41
41
  <small><strong><%= number_with_delimiter(llm_summary[:total_input_tokens]) %></strong></small>
42
42
  </div>
43
43
  <div class="d-flex justify-content-between mb-1">
44
- <small class="text-muted">Output</small>
44
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.output") %></small>
45
45
  <small><strong><%= number_with_delimiter(llm_summary[:total_output_tokens]) %></strong></small>
46
46
  </div>
47
47
  <% if llm_summary[:total_tool_calls] > 0 %>
48
48
  <div class="d-flex justify-content-between mb-1">
49
- <small class="text-muted">Tool calls</small>
49
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.tool_calls") %></small>
50
50
  <small><strong><%= llm_summary[:total_tool_calls] %></strong></small>
51
51
  </div>
52
52
  <% end %>
53
53
  <div class="d-flex justify-content-between mb-1">
54
- <small class="text-muted">Total time</small>
54
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.total_time") %></small>
55
55
  <small><strong><%= "%.1f" % llm_summary[:total_duration_ms] %>ms</strong></small>
56
56
  </div>
57
57
  <% if llm_summary[:error_count] > 0 %>
58
58
  <div class="d-flex justify-content-between mb-1">
59
- <small class="text-muted">Errors</small>
59
+ <small class="text-muted"><%= red_t("red.errors.llm_summary.errors") %></small>
60
60
  <small><span class="badge bg-danger"><%= llm_summary[:error_count] %></span></small>
61
61
  </div>
62
62
  <% end %>
@@ -64,14 +64,14 @@
64
64
 
65
65
  <% if llm_summary[:by_model].any? %>
66
66
  <hr class="my-2">
67
- <small class="text-muted d-block mb-2">By model</small>
67
+ <small class="text-muted d-block mb-2"><%= red_t("red.errors.llm_summary.by_model") %></small>
68
68
  <% llm_summary[:by_model].each do |row| %>
69
69
  <div class="d-flex justify-content-between align-items-center mb-1" style="gap: 0.5rem;">
70
- <small style="min-width: 0; overflow-wrap: anywhere;" title="<%= [ row[:provider], row[:model].presence || "unknown" ].compact.join(" · ") %>">
70
+ <small style="min-width: 0; overflow-wrap: anywhere;" title="<%= [ row[:provider], row[:model].presence || red_t("red.common.unknown") ].compact.join(" · ") %>">
71
71
  <% if row[:provider].present? %>
72
72
  <span class="text-muted"><%= row[:provider] %>·</span>
73
73
  <% end %>
74
- <strong><%= row[:model].presence || "unknown" %></strong>
74
+ <strong><%= row[:model].presence || red_t("red.common.unknown") %></strong>
75
75
  </small>
76
76
  <small class="text-muted text-nowrap">
77
77
  <%= row[:calls] %> · <%= number_with_delimiter(row[:tokens]) %>tok
@@ -87,7 +87,7 @@
87
87
  <div class="alert alert-danger py-1 px-2 mb-0 mt-2">
88
88
  <small>
89
89
  <i class="bi bi-exclamation-triangle" aria-hidden="true"></i>
90
- <%= llm_summary[:error_count] %> failed call<%= llm_summary[:error_count] == 1 ? "" : "s" %> — see breadcrumbs trail for details.
90
+ <%= red_tp("red.errors.llm_summary.failed_calls", count: llm_summary[:error_count]) %>
91
91
  </small>
92
92
  </div>
93
93
  <% end %>
@@ -6,19 +6,19 @@
6
6
  <div class="card mb-4" id="section-local-variables">
7
7
  <div class="card-header">
8
8
  <h5 class="mb-0">
9
- <i class="bi bi-braces"></i> Local Variables
10
- <span class="badge bg-secondary"><%= local_vars.size %> captured</span>
9
+ <i class="bi bi-braces"></i> <%= red_t("red.errors.variables.local_title") %>
10
+ <span class="badge bg-secondary"><%= red_tp("red.errors.variables.captured", count: local_vars.size) %></span>
11
11
  </h5>
12
- <small class="text-muted">Variables from the stack frame where the exception was raised</small>
12
+ <small class="text-muted"><%= red_t("red.errors.variables.local_hint") %></small>
13
13
  </div>
14
14
  <div class="card-body p-0">
15
15
  <div class="table-responsive">
16
16
  <table class="table table-sm table-hover mb-0">
17
17
  <thead class="table-light">
18
18
  <tr>
19
- <th width="200">Variable</th>
20
- <th width="120">Type</th>
21
- <th>Value</th>
19
+ <th width="200"><%= red_t("red.errors.variables.column_variable") %></th>
20
+ <th width="120"><%= red_t("red.errors.variables.column_type") %></th>
21
+ <th><%= red_t("red.errors.variables.column_value") %></th>
22
22
  </tr>
23
23
  </thead>
24
24
  <tbody>
@@ -28,11 +28,11 @@
28
28
  <td><small class="text-muted"><%= info["type"] %></small></td>
29
29
  <td>
30
30
  <% if info["filtered"] %>
31
- <span class="badge bg-warning text-dark">[FILTERED]</span>
31
+ <span class="badge bg-warning text-dark"><%= red_t("red.errors.variables.filtered") %></span>
32
32
  <% else %>
33
33
  <code style="word-break: break-all; font-size: 0.85em;"><%= truncate(info["value"].to_s, length: 500) %></code>
34
34
  <% if info["truncated"] %>
35
- <span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;">truncated</span>
35
+ <span class="badge bg-info text-dark ms-1" style="font-size: 0.7em;"><%= red_t("red.errors.variables.truncated") %></span>
36
36
  <% end %>
37
37
  <% end %>
38
38
  </td>