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.
- checksums.yaml +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- 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
|