source_monitor 0.2.0 → 0.3.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,218 @@
1
+ # Turbo Streams Reference
2
+
3
+ ## Concept
4
+
5
+ Turbo Streams deliver page changes as a set of actions to be performed on specific DOM elements. They can append, prepend, replace, update, remove, before, or after.
6
+
7
+ ## Stream Actions
8
+
9
+ | Action | Purpose | Example |
10
+ |--------|---------|---------|
11
+ | `append` | Add to end of container | Add new item to list |
12
+ | `prepend` | Add to start of container | Add newest item first |
13
+ | `replace` | Replace entire element | Update a record |
14
+ | `update` | Replace inner HTML only | Update content, keep element |
15
+ | `remove` | Delete element | Remove deleted record |
16
+ | `before` | Insert before element | Insert above |
17
+ | `after` | Insert after element | Insert below |
18
+
19
+ ## Basic Usage
20
+
21
+ ### Controller Response
22
+
23
+ ```ruby
24
+ # app/controllers/posts_controller.rb
25
+ def create
26
+ @post = Post.new(post_params)
27
+
28
+ respond_to do |format|
29
+ if @post.save
30
+ format.turbo_stream # renders create.turbo_stream.erb
31
+ format.html { redirect_to @post }
32
+ else
33
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("post_form", partial: "form", locals: { post: @post }) }
34
+ format.html { render :new }
35
+ end
36
+ end
37
+ end
38
+ ```
39
+
40
+ ### Turbo Stream Template
41
+
42
+ ```erb
43
+ <%# app/views/posts/create.turbo_stream.erb %>
44
+
45
+ <%# Add new post to list %>
46
+ <%= turbo_stream.prepend "posts", @post %>
47
+
48
+ <%# Clear the form %>
49
+ <%= turbo_stream.replace "post_form", partial: "posts/form", locals: { post: Post.new } %>
50
+
51
+ <%# Update flash message %>
52
+ <%= turbo_stream.update "flash", partial: "shared/flash" %>
53
+
54
+ <%# Update counter %>
55
+ <%= turbo_stream.update "posts_count", html: "#{Post.count} posts" %>
56
+ ```
57
+
58
+ ## Stream Helpers
59
+
60
+ ### Basic Helpers
61
+
62
+ ```erb
63
+ <%# Append partial to container %>
64
+ <%= turbo_stream.append "posts", partial: "posts/post", locals: { post: @post } %>
65
+
66
+ <%# Append renderable (auto-finds partial) %>
67
+ <%= turbo_stream.append "posts", @post %>
68
+
69
+ <%# Prepend to container %>
70
+ <%= turbo_stream.prepend "posts", @post %>
71
+
72
+ <%# Replace element entirely %>
73
+ <%= turbo_stream.replace dom_id(@post), @post %>
74
+
75
+ <%# Update inner HTML %>
76
+ <%= turbo_stream.update dom_id(@post), @post %>
77
+
78
+ <%# Remove element %>
79
+ <%= turbo_stream.remove dom_id(@post) %>
80
+
81
+ <%# Insert before element %>
82
+ <%= turbo_stream.before dom_id(@other_post), @post %>
83
+
84
+ <%# Insert after element %>
85
+ <%= turbo_stream.after dom_id(@other_post), @post %>
86
+ ```
87
+
88
+ ### Inline Content
89
+
90
+ ```erb
91
+ <%# With HTML string %>
92
+ <%= turbo_stream.update "counter", html: "<strong>5</strong> items" %>
93
+
94
+ <%# With text %>
95
+ <%= turbo_stream.update "status", text: "Processing complete" %>
96
+
97
+ <%# With block %>
98
+ <%= turbo_stream.update "notification" do %>
99
+ <div class="alert alert-success">
100
+ Post created successfully!
101
+ </div>
102
+ <% end %>
103
+ ```
104
+
105
+ ## Real-time with ActionCable
106
+
107
+ ### Broadcast from Model
108
+
109
+ ```ruby
110
+ # app/models/post.rb
111
+ class Post < ApplicationRecord
112
+ after_create_commit { broadcast_prepend_to "posts" }
113
+ after_update_commit { broadcast_replace_to "posts" }
114
+ after_destroy_commit { broadcast_remove_to "posts" }
115
+ end
116
+ ```
117
+
118
+ ### Subscribe in View
119
+
120
+ ```erb
121
+ <%# Subscribe to stream %>
122
+ <%= turbo_stream_from "posts" %>
123
+
124
+ <%# Container that receives updates %>
125
+ <div id="posts">
126
+ <%= render @posts %>
127
+ </div>
128
+ ```
129
+
130
+ ### Broadcast from Controller/Job
131
+
132
+ ```ruby
133
+ # Broadcast to all subscribers
134
+ Turbo::StreamsChannel.broadcast_prepend_to(
135
+ "posts",
136
+ target: "posts",
137
+ partial: "posts/post",
138
+ locals: { post: @post }
139
+ )
140
+
141
+ # Or use helper
142
+ broadcast_prepend_to "posts", target: "posts", partial: "posts/post", locals: { post: @post }
143
+ ```
144
+
145
+ ## Multiple Streams Response
146
+
147
+ ```erb
148
+ <%# app/views/comments/create.turbo_stream.erb %>
149
+
150
+ <%# Add comment to list %>
151
+ <%= turbo_stream.append "comments", @comment %>
152
+
153
+ <%# Update comment count %>
154
+ <%= turbo_stream.update "comment_count" do %>
155
+ <%= pluralize(@post.comments.count, "comment") %>
156
+ <% end %>
157
+
158
+ <%# Clear form %>
159
+ <%= turbo_stream.replace "new_comment" do %>
160
+ <%= render "comments/form", comment: Comment.new(post: @post) %>
161
+ <% end %>
162
+
163
+ <%# Show flash %>
164
+ <%= turbo_stream.prepend "flashes" do %>
165
+ <div class="flash flash-success">Comment added!</div>
166
+ <% end %>
167
+ ```
168
+
169
+ ## Testing Turbo Streams
170
+
171
+ ```ruby
172
+ # test/controllers/posts_controller_test.rb
173
+ require "test_helper"
174
+
175
+ class PostsControllerTest < ActionDispatch::IntegrationTest
176
+ test "returns turbo stream on success" do
177
+ post posts_path,
178
+ params: { post: { title: "Test" } },
179
+ headers: { "Accept" => "text/vnd.turbo-stream.html" }
180
+
181
+ assert_equal "text/vnd.turbo-stream.html", response.media_type
182
+ assert_includes response.body, 'turbo-stream action="prepend"'
183
+ end
184
+ end
185
+ ```
186
+
187
+ ## Common Patterns
188
+
189
+ ### Flash Messages
190
+
191
+ ```erb
192
+ <%# Layout %>
193
+ <div id="flashes">
194
+ <%= render "shared/flash" %>
195
+ </div>
196
+
197
+ <%# In turbo_stream response %>
198
+ <%= turbo_stream.update "flashes", partial: "shared/flash" %>
199
+ ```
200
+
201
+ ### Form Errors
202
+
203
+ ```erb
204
+ <%# On validation failure %>
205
+ <%= turbo_stream.replace "post_form" do %>
206
+ <%= render "form", post: @post %>
207
+ <% end %>
208
+ ```
209
+
210
+ ### Live Counter
211
+
212
+ ```erb
213
+ <%# Initial render %>
214
+ <span id="online_count"><%= @online_count %></span>
215
+
216
+ <%# Broadcast update %>
217
+ <%= turbo_stream.update "online_count", html: @new_count.to_s %>
218
+ ```
@@ -0,0 +1,320 @@
1
+ ---
2
+ name: i18n-patterns
3
+ description: Implements internationalization with Rails I18n for multi-language support. Use when adding translations, managing locales, localizing dates/currencies, pluralization, or when user mentions i18n, translations, locales, or multi-language.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # I18n Patterns for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Rails I18n provides internationalization support:
12
+ - Translation lookups
13
+ - Locale management
14
+ - Date/time/currency formatting
15
+ - Pluralization rules
16
+ - Lazy lookups in views
17
+
18
+ ## Quick Start
19
+
20
+ ```ruby
21
+ # config/application.rb
22
+ config.i18n.default_locale = :en
23
+ config.i18n.available_locales = [:en, :fr, :de]
24
+ config.i18n.fallbacks = true
25
+ ```
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ config/locales/
31
+ ├── en.yml # English defaults
32
+ ├── fr.yml # French defaults
33
+ ├── models/
34
+ │ ├── en.yml # Model translations
35
+ │ └── fr.yml
36
+ ├── views/
37
+ │ ├── en.yml # View translations
38
+ │ └── fr.yml
39
+ ├── mailers/
40
+ │ ├── en.yml
41
+ │ └── fr.yml
42
+ └── components/
43
+ ├── en.yml
44
+ └── fr.yml
45
+ ```
46
+
47
+ ## Locale File Organization
48
+
49
+ ### Models
50
+
51
+ ```yaml
52
+ # config/locales/models/en.yml
53
+ en:
54
+ activerecord:
55
+ models:
56
+ event: Event
57
+ attributes:
58
+ event:
59
+ name: Name
60
+ event_date: Event Date
61
+ status: Status
62
+ event/statuses:
63
+ draft: Draft
64
+ confirmed: Confirmed
65
+ cancelled: Cancelled
66
+ errors:
67
+ models:
68
+ event:
69
+ attributes:
70
+ name:
71
+ blank: "can't be blank"
72
+ ```
73
+
74
+ ### Views
75
+
76
+ ```yaml
77
+ # config/locales/views/en.yml
78
+ en:
79
+ events:
80
+ index:
81
+ title: Events
82
+ new_event: New Event
83
+ no_events: No events found
84
+ show:
85
+ edit: Edit
86
+ delete: Delete
87
+ confirm_delete: Are you sure?
88
+ create:
89
+ success: Event was successfully created.
90
+ destroy:
91
+ success: Event was successfully deleted.
92
+ ```
93
+
94
+ ### Common/Shared
95
+
96
+ ```yaml
97
+ # config/locales/en.yml
98
+ en:
99
+ common:
100
+ actions:
101
+ save: Save
102
+ cancel: Cancel
103
+ delete: Delete
104
+ edit: Edit
105
+ back: Back
106
+ search: Search
107
+ messages:
108
+ loading: Loading...
109
+ no_results: No results found
110
+ not_specified: Not specified
111
+ ```
112
+
113
+ ## Usage Patterns
114
+
115
+ ### In Views (Lazy Lookup)
116
+
117
+ ```erb
118
+ <%# t(".title") resolves to "events.index.title" %>
119
+ <h1><%= t(".title") %></h1>
120
+
121
+ <%# With interpolation %>
122
+ <p><%= t(".welcome", name: current_user.name) %></p>
123
+
124
+ <%# With HTML (use _html suffix) %>
125
+ <p><%= t(".intro_html", link: link_to("here", help_path)) %></p>
126
+ ```
127
+
128
+ ### In Controllers
129
+
130
+ ```ruby
131
+ class EventsController < ApplicationController
132
+ def create
133
+ @event = current_account.events.build(event_params)
134
+ if @event.save
135
+ redirect_to @event, notice: t(".success")
136
+ else
137
+ render :new, status: :unprocessable_entity
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
143
+ ### In Models
144
+
145
+ ```ruby
146
+ class Event < ApplicationRecord
147
+ def status_text
148
+ I18n.t("activerecord.attributes.event/statuses.#{status}")
149
+ end
150
+ end
151
+ ```
152
+
153
+ ### In Presenters
154
+
155
+ ```ruby
156
+ class EventPresenter < BasePresenter
157
+ def formatted_date
158
+ return not_specified if event_date.nil?
159
+ I18n.l(event_date, format: :long)
160
+ end
161
+
162
+ private
163
+
164
+ def not_specified
165
+ tag.span(I18n.t("common.messages.not_specified"), class: "text-slate-400 italic")
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## Date/Time/Number Formatting
171
+
172
+ ```ruby
173
+ I18n.l(Date.current) # "January 15, 2024"
174
+ I18n.l(Date.current, format: :short) # "Jan 15"
175
+ I18n.l(Date.current, format: :long) # "Wednesday, January 15, 2024"
176
+
177
+ number_to_currency(1234.50) # "$1,234.50"
178
+ number_to_currency(1234.50, locale: :fr) # "1 234,50 EUR"
179
+ ```
180
+
181
+ ## Pluralization
182
+
183
+ ```yaml
184
+ en:
185
+ events:
186
+ count:
187
+ zero: No events
188
+ one: 1 event
189
+ other: "%{count} events"
190
+ ```
191
+
192
+ ```ruby
193
+ t("events.count", count: 0) # "No events"
194
+ t("events.count", count: 1) # "1 event"
195
+ t("events.count", count: 5) # "5 events"
196
+ ```
197
+
198
+ ## Locale Switching
199
+
200
+ ### URL-Based
201
+
202
+ ```ruby
203
+ # config/routes.rb
204
+ scope "(:locale)", locale: /en|fr|de/ do
205
+ resources :events
206
+ end
207
+
208
+ # app/controllers/application_controller.rb
209
+ class ApplicationController < ActionController::Base
210
+ around_action :switch_locale
211
+
212
+ private
213
+
214
+ def switch_locale(&action)
215
+ locale = params[:locale] || I18n.default_locale
216
+ I18n.with_locale(locale, &action)
217
+ end
218
+
219
+ def default_url_options
220
+ { locale: I18n.locale }
221
+ end
222
+ end
223
+ ```
224
+
225
+ ### User Preference
226
+
227
+ ```ruby
228
+ def switch_locale(&action)
229
+ locale = current_user&.locale || extract_locale_from_header || I18n.default_locale
230
+ I18n.with_locale(locale, &action)
231
+ end
232
+ ```
233
+
234
+ ## Testing I18n
235
+
236
+ ### Missing Translation Detection
237
+
238
+ ```ruby
239
+ # test/i18n_test.rb
240
+ require "test_helper"
241
+
242
+ class I18nTest < ActiveSupport::TestCase
243
+ test "no missing translations for English" do
244
+ # Use i18n-tasks gem for comprehensive checks
245
+ # Or manually verify critical paths
246
+ assert I18n.t("events.index.title", locale: :en).present?
247
+ assert I18n.t("events.create.success", locale: :en).present?
248
+ end
249
+
250
+ test "all available locales have required keys" do
251
+ required_keys = %w[
252
+ events.index.title
253
+ events.create.success
254
+ common.actions.save
255
+ common.actions.cancel
256
+ ]
257
+
258
+ I18n.available_locales.each do |locale|
259
+ required_keys.each do |key|
260
+ translation = I18n.t(key, locale: locale, raise: true)
261
+ assert translation.present?, "Missing #{locale}.#{key}"
262
+ end
263
+ end
264
+ end
265
+ end
266
+ ```
267
+
268
+ ### View Translation Test
269
+
270
+ ```ruby
271
+ # test/controllers/events_controller_test.rb
272
+ require "test_helper"
273
+
274
+ class EventsI18nTest < ActionDispatch::IntegrationTest
275
+ setup do
276
+ sign_in users(:one)
277
+ end
278
+
279
+ test "index page uses translations" do
280
+ get events_path
281
+ assert_response :success
282
+ assert_includes response.body, I18n.t("events.index.title")
283
+ end
284
+
285
+ test "index page works in French" do
286
+ get events_path(locale: :fr)
287
+ assert_response :success
288
+ assert_includes response.body, I18n.t("events.index.title", locale: :fr)
289
+ end
290
+ end
291
+ ```
292
+
293
+ ## i18n-tasks Gem
294
+
295
+ ```bash
296
+ bundle exec i18n-tasks missing # Find missing translations
297
+ bundle exec i18n-tasks unused # Find unused translations
298
+ bundle exec i18n-tasks normalize # Normalize locale files
299
+ bundle exec i18n-tasks health # Health check
300
+ ```
301
+
302
+ ## Best Practices
303
+
304
+ - Use lazy lookups in views: `t(".title")` not `t("events.index.title")`
305
+ - Use `_html` suffix for HTML content
306
+ - Use interpolation for dynamic content: `t(".greeting", name: name)`
307
+ - Organize locale files by domain (models, views, mailers)
308
+ - Never hardcode user-facing strings in views
309
+ - Never concatenate translations
310
+
311
+ ## Checklist
312
+
313
+ - [ ] Locale files organized by domain
314
+ - [ ] All user-facing text uses I18n
315
+ - [ ] Lazy lookups in views
316
+ - [ ] Pluralization for countable items
317
+ - [ ] Date/currency formatting localized
318
+ - [ ] Locale switching implemented
319
+ - [ ] Missing translation detection in tests
320
+ - [ ] All tests GREEN