claude-agent-framework 1.0.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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Skill: Hotwire
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Implement dynamic, SPA-like interactions using Turbo (Drive, Frames, Streams) and Stimulus without writing custom JavaScript.
|
|
5
|
+
|
|
6
|
+
## Turbo Drive
|
|
7
|
+
|
|
8
|
+
### Automatic Enhancement
|
|
9
|
+
Turbo Drive is enabled by default. All links and forms are enhanced automatically.
|
|
10
|
+
|
|
11
|
+
### Disabling Turbo
|
|
12
|
+
```erb
|
|
13
|
+
<%# Disable for specific link %>
|
|
14
|
+
<%= link_to "External", "https://example.com", data: { turbo: false } %>
|
|
15
|
+
|
|
16
|
+
<%# Disable for form %>
|
|
17
|
+
<%= form_with model: @article, data: { turbo: false } do |f| %>
|
|
18
|
+
<% end %>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Progress Bar
|
|
22
|
+
```css
|
|
23
|
+
/* Customize in application.css */
|
|
24
|
+
.turbo-progress-bar {
|
|
25
|
+
height: 3px;
|
|
26
|
+
background-color: #3b82f6;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Turbo Frames
|
|
31
|
+
|
|
32
|
+
### Basic Frame
|
|
33
|
+
```erb
|
|
34
|
+
<%# index.html.erb %>
|
|
35
|
+
<%= turbo_frame_tag "articles" do %>
|
|
36
|
+
<%= render @articles %>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
39
|
+
<%# _article.html.erb %>
|
|
40
|
+
<%= turbo_frame_tag dom_id(article) do %>
|
|
41
|
+
<div class="article-card">
|
|
42
|
+
<%= article.title %>
|
|
43
|
+
<%= link_to "Edit", edit_article_path(article) %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
47
|
+
<%# edit.html.erb %>
|
|
48
|
+
<%= turbo_frame_tag dom_id(@article) do %>
|
|
49
|
+
<%= render "form", article: @article %>
|
|
50
|
+
<% end %>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Lazy Loading
|
|
54
|
+
```erb
|
|
55
|
+
<%= turbo_frame_tag "comments",
|
|
56
|
+
src: article_comments_path(@article),
|
|
57
|
+
loading: :lazy do %>
|
|
58
|
+
<p>Loading comments...</p>
|
|
59
|
+
<% end %>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Breaking Out of Frames
|
|
63
|
+
```erb
|
|
64
|
+
<%# Target entire page %>
|
|
65
|
+
<%= link_to "View All", articles_path, data: { turbo_frame: "_top" } %>
|
|
66
|
+
|
|
67
|
+
<%# Target specific frame %>
|
|
68
|
+
<%= link_to "Details", article_path(article), data: { turbo_frame: "main_content" } %>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Frame Targeting
|
|
72
|
+
```erb
|
|
73
|
+
<%# Form targets different frame %>
|
|
74
|
+
<%= form_with model: @comment, data: { turbo_frame: "comments" } do |f| %>
|
|
75
|
+
<%= f.text_area :body %>
|
|
76
|
+
<%= f.submit %>
|
|
77
|
+
<% end %>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Turbo Streams
|
|
81
|
+
|
|
82
|
+
### Stream Actions
|
|
83
|
+
```erb
|
|
84
|
+
<%# append - Add to end %>
|
|
85
|
+
<%= turbo_stream.append "articles", @article %>
|
|
86
|
+
|
|
87
|
+
<%# prepend - Add to beginning %>
|
|
88
|
+
<%= turbo_stream.prepend "articles", @article %>
|
|
89
|
+
|
|
90
|
+
<%# replace - Replace entire element %>
|
|
91
|
+
<%= turbo_stream.replace @article %>
|
|
92
|
+
|
|
93
|
+
<%# update - Replace contents only %>
|
|
94
|
+
<%= turbo_stream.update "counter", "42" %>
|
|
95
|
+
|
|
96
|
+
<%# remove - Delete element %>
|
|
97
|
+
<%= turbo_stream.remove @article %>
|
|
98
|
+
|
|
99
|
+
<%# before/after - Insert adjacent %>
|
|
100
|
+
<%= turbo_stream.before @article, partial: "divider" %>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Controller Response
|
|
104
|
+
```ruby
|
|
105
|
+
def create
|
|
106
|
+
@article = current_user.articles.build(article_params)
|
|
107
|
+
|
|
108
|
+
if @article.save
|
|
109
|
+
respond_to do |format|
|
|
110
|
+
format.html { redirect_to @article }
|
|
111
|
+
format.turbo_stream
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
render :new, status: :unprocessable_entity
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Stream Template
|
|
120
|
+
```erb
|
|
121
|
+
<%# app/views/articles/create.turbo_stream.erb %>
|
|
122
|
+
<%= turbo_stream.prepend "articles", @article %>
|
|
123
|
+
<%= turbo_stream.update "articles_count", Article.count %>
|
|
124
|
+
<%= turbo_stream.replace "new_article_form" do %>
|
|
125
|
+
<%= render "form", article: Article.new %>
|
|
126
|
+
<% end %>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Inline Streams
|
|
130
|
+
```ruby
|
|
131
|
+
def destroy
|
|
132
|
+
@article.destroy
|
|
133
|
+
|
|
134
|
+
render turbo_stream: turbo_stream.remove(@article)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Multiple streams
|
|
138
|
+
render turbo_stream: [
|
|
139
|
+
turbo_stream.remove(@article),
|
|
140
|
+
turbo_stream.update("count", Article.count)
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Stimulus Controllers
|
|
145
|
+
|
|
146
|
+
### Basic Controller
|
|
147
|
+
```javascript
|
|
148
|
+
// app/javascript/controllers/hello_controller.js
|
|
149
|
+
import { Controller } from "@hotwired/stimulus"
|
|
150
|
+
|
|
151
|
+
export default class extends Controller {
|
|
152
|
+
static targets = ["name", "output"]
|
|
153
|
+
static values = { greeting: { type: String, default: "Hello" } }
|
|
154
|
+
|
|
155
|
+
greet() {
|
|
156
|
+
this.outputTarget.textContent = `${this.greetingValue}, ${this.nameTarget.value}!`
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### HTML Usage
|
|
162
|
+
```erb
|
|
163
|
+
<div data-controller="hello" data-hello-greeting-value="Hi">
|
|
164
|
+
<input data-hello-target="name" type="text">
|
|
165
|
+
<button data-action="click->hello#greet">Greet</button>
|
|
166
|
+
<span data-hello-target="output"></span>
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Common Controllers
|
|
171
|
+
|
|
172
|
+
#### Toggle
|
|
173
|
+
```javascript
|
|
174
|
+
// toggle_controller.js
|
|
175
|
+
import { Controller } from "@hotwired/stimulus"
|
|
176
|
+
|
|
177
|
+
export default class extends Controller {
|
|
178
|
+
static targets = ["content"]
|
|
179
|
+
static classes = ["hidden"]
|
|
180
|
+
|
|
181
|
+
toggle() {
|
|
182
|
+
this.contentTarget.classList.toggle(this.hiddenClass)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Flash Auto-Dismiss
|
|
188
|
+
```javascript
|
|
189
|
+
// flash_controller.js
|
|
190
|
+
import { Controller } from "@hotwired/stimulus"
|
|
191
|
+
|
|
192
|
+
export default class extends Controller {
|
|
193
|
+
connect() {
|
|
194
|
+
setTimeout(() => this.dismiss(), 5000)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dismiss() {
|
|
198
|
+
this.element.remove()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Form Validation
|
|
204
|
+
```javascript
|
|
205
|
+
// form_validation_controller.js
|
|
206
|
+
import { Controller } from "@hotwired/stimulus"
|
|
207
|
+
|
|
208
|
+
export default class extends Controller {
|
|
209
|
+
static targets = ["submit"]
|
|
210
|
+
|
|
211
|
+
validate(event) {
|
|
212
|
+
const form = event.target.closest("form")
|
|
213
|
+
this.submitTarget.disabled = !form.checkValidity()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Dropdown
|
|
219
|
+
```javascript
|
|
220
|
+
// dropdown_controller.js
|
|
221
|
+
import { Controller } from "@hotwired/stimulus"
|
|
222
|
+
|
|
223
|
+
export default class extends Controller {
|
|
224
|
+
static targets = ["menu"]
|
|
225
|
+
|
|
226
|
+
toggle() {
|
|
227
|
+
this.menuTarget.classList.toggle("hidden")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
close(event) {
|
|
231
|
+
if (!this.element.contains(event.target)) {
|
|
232
|
+
this.menuTarget.classList.add("hidden")
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
connect() {
|
|
237
|
+
document.addEventListener("click", this.close.bind(this))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
disconnect() {
|
|
241
|
+
document.removeEventListener("click", this.close.bind(this))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Real-Time with Turbo Streams
|
|
247
|
+
|
|
248
|
+
### Broadcast from Model
|
|
249
|
+
```ruby
|
|
250
|
+
class Comment < ApplicationRecord
|
|
251
|
+
belongs_to :article
|
|
252
|
+
|
|
253
|
+
after_create_commit -> {
|
|
254
|
+
broadcast_prepend_to article, target: "comments"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
after_destroy_commit -> {
|
|
258
|
+
broadcast_remove_to article
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### View Subscription
|
|
264
|
+
```erb
|
|
265
|
+
<%= turbo_stream_from @article %>
|
|
266
|
+
|
|
267
|
+
<div id="comments">
|
|
268
|
+
<%= render @article.comments %>
|
|
269
|
+
</div>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Best Practices
|
|
273
|
+
|
|
274
|
+
1. **Use frames for in-place editing** - Edit forms replace show content
|
|
275
|
+
2. **Use streams for lists** - Append/prepend without full reload
|
|
276
|
+
3. **Keep Stimulus simple** - Small, focused controllers
|
|
277
|
+
4. **Leverage morphing** - For complex updates
|
|
278
|
+
5. **Progressive enhancement** - Works without JS first
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Skill: Internationalization (i18n)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Implement multi-language support using Rails built-in i18n framework.
|
|
5
|
+
|
|
6
|
+
## Configuration
|
|
7
|
+
|
|
8
|
+
### Available Locales
|
|
9
|
+
```ruby
|
|
10
|
+
# config/application.rb
|
|
11
|
+
config.i18n.available_locales = [:en, :es, :fr]
|
|
12
|
+
config.i18n.default_locale = :en
|
|
13
|
+
config.i18n.fallbacks = true
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Load Path
|
|
17
|
+
```ruby
|
|
18
|
+
# config/application.rb
|
|
19
|
+
config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.{rb,yml}")]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Locale Files Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
config/locales/
|
|
26
|
+
├── en.yml # Default English
|
|
27
|
+
├── es.yml # Spanish
|
|
28
|
+
├── models/
|
|
29
|
+
│ ├── user.en.yml
|
|
30
|
+
│ └── user.es.yml
|
|
31
|
+
├── views/
|
|
32
|
+
│ ├── articles.en.yml
|
|
33
|
+
│ └── articles.es.yml
|
|
34
|
+
└── defaults/
|
|
35
|
+
├── en.yml
|
|
36
|
+
└── es.yml
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Translations
|
|
40
|
+
|
|
41
|
+
### Simple Strings
|
|
42
|
+
```yaml
|
|
43
|
+
# config/locales/en.yml
|
|
44
|
+
en:
|
|
45
|
+
hello: "Hello"
|
|
46
|
+
welcome: "Welcome to our app"
|
|
47
|
+
|
|
48
|
+
# config/locales/es.yml
|
|
49
|
+
es:
|
|
50
|
+
hello: "Hola"
|
|
51
|
+
welcome: "Bienvenido a nuestra app"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Nested Structure
|
|
55
|
+
```yaml
|
|
56
|
+
# config/locales/en.yml
|
|
57
|
+
en:
|
|
58
|
+
navigation:
|
|
59
|
+
home: "Home"
|
|
60
|
+
about: "About"
|
|
61
|
+
contact: "Contact"
|
|
62
|
+
|
|
63
|
+
footer:
|
|
64
|
+
copyright: "All rights reserved"
|
|
65
|
+
privacy: "Privacy Policy"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### With Variables
|
|
69
|
+
```yaml
|
|
70
|
+
en:
|
|
71
|
+
greeting: "Hello, %{name}!"
|
|
72
|
+
items_count: "You have %{count} items"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
t("greeting", name: "John") # => "Hello, John!"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Pluralization
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
en:
|
|
83
|
+
articles:
|
|
84
|
+
count:
|
|
85
|
+
zero: "No articles"
|
|
86
|
+
one: "1 article"
|
|
87
|
+
other: "%{count} articles"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
t("articles.count", count: 0) # => "No articles"
|
|
92
|
+
t("articles.count", count: 1) # => "1 article"
|
|
93
|
+
t("articles.count", count: 5) # => "5 articles"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Model Translations
|
|
97
|
+
|
|
98
|
+
### Active Record Attributes
|
|
99
|
+
```yaml
|
|
100
|
+
# config/locales/models/article.en.yml
|
|
101
|
+
en:
|
|
102
|
+
activerecord:
|
|
103
|
+
models:
|
|
104
|
+
article:
|
|
105
|
+
one: "Article"
|
|
106
|
+
other: "Articles"
|
|
107
|
+
attributes:
|
|
108
|
+
article:
|
|
109
|
+
title: "Title"
|
|
110
|
+
body: "Content"
|
|
111
|
+
published: "Published"
|
|
112
|
+
created_at: "Created"
|
|
113
|
+
errors:
|
|
114
|
+
models:
|
|
115
|
+
article:
|
|
116
|
+
attributes:
|
|
117
|
+
title:
|
|
118
|
+
blank: "Please provide a title"
|
|
119
|
+
too_short: "Title must be at least %{count} characters"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Usage in Views
|
|
123
|
+
```erb
|
|
124
|
+
<%= Article.model_name.human %>
|
|
125
|
+
<%= Article.human_attribute_name(:title) %>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## View Translations
|
|
129
|
+
|
|
130
|
+
### Lazy Lookup
|
|
131
|
+
```yaml
|
|
132
|
+
# config/locales/views/articles.en.yml
|
|
133
|
+
en:
|
|
134
|
+
articles:
|
|
135
|
+
index:
|
|
136
|
+
title: "All Articles"
|
|
137
|
+
new_article: "New Article"
|
|
138
|
+
no_articles: "No articles yet"
|
|
139
|
+
show:
|
|
140
|
+
edit: "Edit"
|
|
141
|
+
delete: "Delete"
|
|
142
|
+
confirm_delete: "Are you sure?"
|
|
143
|
+
form:
|
|
144
|
+
submit_create: "Create Article"
|
|
145
|
+
submit_update: "Update Article"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```erb
|
|
149
|
+
<%# app/views/articles/index.html.erb %>
|
|
150
|
+
<h1><%= t(".title") %></h1>
|
|
151
|
+
<%= link_to t(".new_article"), new_article_path %>
|
|
152
|
+
|
|
153
|
+
<% if @articles.empty? %>
|
|
154
|
+
<p><%= t(".no_articles") %></p>
|
|
155
|
+
<% end %>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### With HTML
|
|
159
|
+
```yaml
|
|
160
|
+
en:
|
|
161
|
+
welcome_html: "Welcome to <strong>Our App</strong>"
|
|
162
|
+
terms_html: "By signing up, you agree to our %{link}"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```erb
|
|
166
|
+
<%= t("welcome_html") %>
|
|
167
|
+
<%= t("terms_html", link: link_to(t("terms"), terms_path)) %>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Date and Time
|
|
171
|
+
|
|
172
|
+
### Formats
|
|
173
|
+
```yaml
|
|
174
|
+
en:
|
|
175
|
+
date:
|
|
176
|
+
formats:
|
|
177
|
+
default: "%Y-%m-%d"
|
|
178
|
+
short: "%b %d"
|
|
179
|
+
long: "%B %d, %Y"
|
|
180
|
+
time:
|
|
181
|
+
formats:
|
|
182
|
+
default: "%Y-%m-%d %H:%M"
|
|
183
|
+
short: "%d %b %H:%M"
|
|
184
|
+
long: "%B %d, %Y at %H:%M"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Usage
|
|
188
|
+
```erb
|
|
189
|
+
<%= l(article.created_at) %>
|
|
190
|
+
<%= l(article.created_at, format: :short) %>
|
|
191
|
+
<%= l(article.created_at, format: :long) %>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Controller Setup
|
|
195
|
+
|
|
196
|
+
### Set Locale
|
|
197
|
+
```ruby
|
|
198
|
+
# app/controllers/application_controller.rb
|
|
199
|
+
class ApplicationController < ActionController::Base
|
|
200
|
+
around_action :switch_locale
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def switch_locale(&action)
|
|
205
|
+
locale = extract_locale || I18n.default_locale
|
|
206
|
+
I18n.with_locale(locale, &action)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def extract_locale
|
|
210
|
+
# From URL param
|
|
211
|
+
locale = params[:locale]
|
|
212
|
+
# From user preference
|
|
213
|
+
locale ||= current_user&.locale
|
|
214
|
+
# From Accept-Language header
|
|
215
|
+
locale ||= extract_locale_from_header
|
|
216
|
+
|
|
217
|
+
locale if I18n.available_locales.include?(locale&.to_sym)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def extract_locale_from_header
|
|
221
|
+
request.env["HTTP_ACCEPT_LANGUAGE"]&.scan(/^[a-z]{2}/)&.first
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def default_url_options
|
|
225
|
+
{ locale: I18n.locale }
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### URL-Based Locale
|
|
231
|
+
```ruby
|
|
232
|
+
# config/routes.rb
|
|
233
|
+
Rails.application.routes.draw do
|
|
234
|
+
scope "(:locale)", locale: /en|es|fr/ do
|
|
235
|
+
resources :articles
|
|
236
|
+
root "pages#home"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## JavaScript Translations
|
|
242
|
+
|
|
243
|
+
### Export to JS
|
|
244
|
+
```ruby
|
|
245
|
+
# config/initializers/i18n_js.rb
|
|
246
|
+
# Using i18n-js gem
|
|
247
|
+
I18nJS.configure do |config|
|
|
248
|
+
config.translations do |t|
|
|
249
|
+
t.file = "app/javascript/translations.json"
|
|
250
|
+
t.patterns << "*"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Stimulus Controller
|
|
256
|
+
```javascript
|
|
257
|
+
// app/javascript/controllers/i18n_controller.js
|
|
258
|
+
import { Controller } from "@hotwired/stimulus"
|
|
259
|
+
import translations from "../translations.json"
|
|
260
|
+
|
|
261
|
+
export default class extends Controller {
|
|
262
|
+
static values = { locale: String }
|
|
263
|
+
|
|
264
|
+
t(key, options = {}) {
|
|
265
|
+
let translation = translations[this.localeValue][key]
|
|
266
|
+
|
|
267
|
+
Object.keys(options).forEach(k => {
|
|
268
|
+
translation = translation.replace(`%{${k}}`, options[k])
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return translation
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Form Errors
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
en:
|
|
280
|
+
errors:
|
|
281
|
+
format: "%{attribute} %{message}"
|
|
282
|
+
messages:
|
|
283
|
+
blank: "can't be blank"
|
|
284
|
+
invalid: "is invalid"
|
|
285
|
+
too_short: "is too short (minimum is %{count} characters)"
|
|
286
|
+
too_long: "is too long (maximum is %{count} characters)"
|
|
287
|
+
taken: "has already been taken"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Flash Messages
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
en:
|
|
294
|
+
flash:
|
|
295
|
+
actions:
|
|
296
|
+
create:
|
|
297
|
+
success: "%{model} was successfully created."
|
|
298
|
+
error: "Could not create %{model}."
|
|
299
|
+
update:
|
|
300
|
+
success: "%{model} was successfully updated."
|
|
301
|
+
error: "Could not update %{model}."
|
|
302
|
+
destroy:
|
|
303
|
+
success: "%{model} was successfully deleted."
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
# Controller
|
|
308
|
+
redirect_to @article, notice: t("flash.actions.create.success", model: Article.model_name.human)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Testing
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# spec/support/i18n.rb
|
|
315
|
+
RSpec.configure do |config|
|
|
316
|
+
config.before(:each) do
|
|
317
|
+
I18n.locale = :en
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Test missing translations
|
|
322
|
+
I18n.exception_handler = ->(exception, locale, key, options) {
|
|
323
|
+
raise exception.to_exception
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Best Practices
|
|
328
|
+
|
|
329
|
+
1. **Use lazy lookup** - `.title` instead of full path
|
|
330
|
+
2. **Organize by feature** - Separate files per domain
|
|
331
|
+
3. **Use YAML anchors** - Reduce duplication
|
|
332
|
+
4. **Test all locales** - Ensure no missing keys
|
|
333
|
+
5. **Use variables** - Don't concatenate strings
|
|
334
|
+
6. **Keep defaults in en.yml** - Fallback always works
|