forked from
smokesignal.events/smokesignal
Fork i18n + search + filtering- v0.2
1# Smokesignal Template Migration Guidelines
2
3This document provides step-by-step guidance for migrating existing hardcoded templates to use the new i18n system with on-demand translation functions.
4
5## Migration Overview
6
7Migrate from hardcoded strings in templates to Fluent-based translations using template functions. This migration eliminates pre-rendered translation HashMaps and improves HTMX performance.
8
9## Migration Strategy
10
11### Phase 1: Template Analysis & Key Extraction
12
13#### 1.1 Inventory Existing Strings
14```bash
15# Find all hardcoded strings in templates
16find templates/ -name "*.html" -exec grep -Hn '"[^"]*"' {} \; > strings_inventory.txt
17find templates/ -name "*.html" -exec grep -Hn "'[^']*'" {} \; >> strings_inventory.txt
18
19# Categorize by domain for organized migration
20grep -E "(button|btn|submit|save|edit|delete|cancel)" strings_inventory.txt > actions.txt
21grep -E "(error|fail|invalid|required)" strings_inventory.txt > errors.txt
22grep -E "(title|heading|h1|h2|h3)" strings_inventory.txt > headings.txt
23grep -E "(label|placeholder|hint)" strings_inventory.txt > forms.txt
24```
25
26#### 1.2 Create Translation Key Naming Convention
27```
28# Pattern: domain-purpose[-variant]
29save-changes # Basic action
30edit-profile # Specific action
31validation-required # Error message
32profile-title # Page heading
33enter-name-placeholder # Form guidance
34welcome-message-feminine # Gender variant
35```
36
37### Phase 2: Fluent File Creation
38
39#### 2.1 Organize by Category
40```ftl
41# i18n/en-us/actions.ftl
42save-changes = Save Changes
43edit-profile = Edit Profile
44delete-item = Delete
45cancel-action = Cancel
46follow-user = Follow
47unfollow-user = Unfollow
48
49# i18n/en-us/errors.ftl
50validation-required = This field is required
51validation-email = Please enter a valid email
52validation-minlength = Must be at least {$min} characters
53form-submit-error = Unable to submit form
54profile-not-found = Profile not found
55
56# i18n/en-us/ui.ftl
57profile-title = Profile
58member-since = Member since
59events-created = Events Created
60welcome-message = Welcome
61search-placeholder = Search...
62
63# i18n/fr-ca/actions.ftl
64save-changes = Enregistrer les modifications
65edit-profile = Modifier le profil
66delete-item = Supprimer
67cancel-action = Annuler
68follow-user = Suivre
69unfollow-user = Ne plus suivre
70```
71
72#### 2.2 Gender-Aware Translations
73```ftl
74# English (gender-neutral by default)
75welcome-message = Welcome
76profile-greeting = Hello there
77
78# French Canadian (gender variants)
79welcome-message = Bienvenue
80welcome-message-feminine = Bienvenue
81welcome-message-masculine = Bienvenu
82welcome-message-neutral = Bienvenue
83
84profile-greeting = Bonjour
85profile-greeting-feminine = Bonjour madame
86profile-greeting-masculine = Bonjour monsieur
87profile-greeting-neutral = Bonjour
88```
89
90### Phase 3: Template Function Integration
91
92#### 3.1 Replace Simple Strings
93```html
94<!-- Before -->
95<button class="button">Save Changes</button>
96<h1>Profile</h1>
97<p>Member since {{ profile.created_at }}</p>
98
99<!-- After -->
100<button class="button">{{ t(key="save-changes", locale=locale) }}</button>
101<h1>{{ t(key="profile-title", locale=locale) }}</h1>
102<p>{{ t(key="member-since", locale=locale) }} {{ profile.created_at }}</p>
103```
104
105#### 3.2 Add Gender-Aware Translations
106```html
107<!-- Before -->
108<h2>Welcome, {{ user.name }}!</h2>
109
110<!-- After -->
111<h2>{{ tg(key="welcome-message", locale=locale, gender=user_gender) }}, {{ user.name }}!</h2>
112```
113
114#### 3.3 Handle Parameterized Messages
115```html
116<!-- Before -->
117<p>You have {{ event_count }} events</p>
118
119<!-- After -->
120<p>{{ tc(key="events-count", locale=locale, count=event_count) }}</p>
121```
122
123### Phase 4: HTMX-Specific Migration
124
125#### 4.1 Form Templates with Language Propagation
126```html
127<!-- Before -->
128<form hx-post="/profile/update" hx-target="#profile-content">
129 <label>Display Name</label>
130 <input name="display_name" placeholder="Enter your name" />
131 <button type="submit">Save Changes</button>
132</form>
133
134<!-- After -->
135<form hx-post="/profile/update"
136 hx-target="#profile-content"
137 hx-headers='{"HX-Current-Language": "{{ locale }}"}'>
138
139 <label>{{ t(key="display-name", locale=locale) }}</label>
140 <input name="display_name"
141 placeholder="{{ t(key="enter-name-placeholder", locale=locale) }}" />
142 <button type="submit">{{ t(key="save-changes", locale=locale) }}</button>
143</form>
144```
145
146#### 4.2 Error Message Templates
147```html
148<!-- Before -->
149<div class="error">Invalid email address</div>
150
151<!-- After -->
152<div class="error">{{ t(key="validation-email", locale=locale) }}</div>
153```
154
155### Phase 5: Template Hierarchy Migration
156
157#### 5.1 Base Template Updates
158```html
159<!-- templates/base.en-us.html -->
160<!doctype html>
161<html lang="{{ language }}">
162<head>
163 <title>{{ t(key="site-title", locale=locale) }}</title>
164 <meta name="description" content="{{ t(key="site-description", locale=locale) }}">
165</head>
166<body data-current-language="{{ locale }}">
167 {% include 'nav.html' %}
168 {% block content %}{% endblock %}
169 {% include 'footer.html' %}
170</body>
171</html>
172```
173
174#### 5.2 Partial Templates for HTMX
175```html
176<!-- templates/partials/profile_form.html -->
177<div id="profile-form" data-current-language="{{ locale }}">
178 <h3>{{ t(key="edit-profile-title", locale=locale) }}</h3>
179
180 {% if errors %}
181 <div class="errors">
182 {% for error in errors %}
183 <p class="error">{{ t(key=error.key, locale=locale) }}</p>
184 {% endfor %}
185 </div>
186 {% endif %}
187
188 <form hx-post="/profile/update"
189 hx-target="#profile-content"
190 hx-headers='{"HX-Current-Language": "{{ locale }}"}'>
191 <!-- Form fields with translations -->
192 </form>
193</div>
194```
195
196## Migration Tools & Automation
197
198### Automated String Replacement Script
199```bash
200#!/bin/bash
201# migrate_template.sh
202
203TEMPLATE_FILE=$1
204BACKUP_FILE="${TEMPLATE_FILE}.bak"
205
206# Create backup
207cp "$TEMPLATE_FILE" "$BACKUP_FILE"
208
209# Replace common patterns
210sed -i 's/"Save Changes"/{{ t(key="save-changes", locale=locale) }}/g' "$TEMPLATE_FILE"
211sed -i 's/"Edit Profile"/{{ t(key="edit-profile", locale=locale) }}/g' "$TEMPLATE_FILE"
212sed -i 's/"Delete"/{{ t(key="delete-item", locale=locale) }}/g' "$TEMPLATE_FILE"
213sed -i 's/"Cancel"/{{ t(key="cancel-action", locale=locale) }}/g' "$TEMPLATE_FILE"
214
215# Handle form labels
216sed -i 's/"Display Name"/{{ t(key="display-name", locale=locale) }}/g' "$TEMPLATE_FILE"
217sed -i 's/"Email"/{{ t(key="email", locale=locale) }}/g' "$TEMPLATE_FILE"
218
219echo "Migrated $TEMPLATE_FILE (backup: $BACKUP_FILE)"
220```
221
222### Translation Key Validator
223```rust
224// tools/validate_keys.rs
225use std::collections::HashSet;
226use regex::Regex;
227
228fn extract_translation_keys_from_templates() -> HashSet<String> {
229 let re = Regex::new(r#"\{\{\s*t\w*\(key="([^"]+)""#).unwrap();
230 // Extract all translation keys from templates
231 // Return set of used keys
232}
233
234fn load_fluent_keys() -> HashSet<String> {
235 // Load all keys from .ftl files
236 // Return set of available keys
237}
238
239#[test]
240fn test_all_translation_keys_exist() {
241 let used_keys = extract_translation_keys_from_templates();
242 let available_keys = load_fluent_keys();
243
244 for key in &used_keys {
245 assert!(
246 available_keys.contains(key),
247 "Missing translation key: {} (used in templates)",
248 key
249 );
250 }
251
252 println!("✅ All {} translation keys validated", used_keys.len());
253}
254```
255
256## Migration Validation
257
258### Template Syntax Validation
259```bash
260# Validate template syntax after migration
261find templates/ -name "*.html" -exec python3 -c "
262import sys
263import re
264
265def validate_template(file_path):
266 with open(file_path, 'r') as f:
267 content = f.read()
268
269 # Check for proper function calls
270 pattern = r'\{\{\s*t[gc]?\(key=[\"'\''][^\"\']+[\"'\''][^}]*\)\s*\}\}'
271 matches = re.findall(pattern, content)
272
273 # Check for missing locale parameter
274 missing_locale = re.findall(r'\{\{\s*t[gc]?\([^}]*\)\s*\}\}', content)
275
276 print(f'File: {file_path}')
277 print(f' Translation calls: {len(matches)}')
278 if missing_locale:
279 print(f' ⚠️ Potential missing locale: {len(missing_locale)}')
280
281validate_template(sys.argv[1])
282" {} \;
283```
284
285### Performance Comparison
286```rust
287// Compare before/after performance
288#[cfg(test)]
289mod migration_performance_tests {
290 #[test]
291 fn benchmark_old_vs_new_rendering() {
292 // Test pre-rendered HashMap approach vs on-demand functions
293 let start = std::time::Instant::now();
294
295 // Old approach: pre-render all translations
296 let _old_result = render_with_prerendered_translations();
297 let old_duration = start.elapsed();
298
299 let start = std::time::Instant::now();
300
301 // New approach: on-demand translation functions
302 let _new_result = render_with_template_functions();
303 let new_duration = start.elapsed();
304
305 println!("Old approach: {:?}", old_duration);
306 println!("New approach: {:?}", new_duration);
307
308 // Expect significant improvement
309 assert!(new_duration < old_duration * 3 / 4);
310 }
311}
312```
313
314## Migration Checklist
315
316### Per Template
317- [ ] Backup original template
318- [ ] Extract all hardcoded strings
319- [ ] Create corresponding Fluent keys
320- [ ] Replace strings with template functions
321- [ ] Add HTMX language headers if applicable
322- [ ] Test rendering in both languages
323- [ ] Validate gender variants (if applicable)
324- [ ] Performance test HTMX interactions
325
326### Per Handler
327- [ ] Remove pre-rendered translation HashMap
328- [ ] Use minimal template context
329- [ ] Ensure Language extractor is used
330- [ ] Add proper error handling for missing keys
331- [ ] Test with HTMX requests
332
333### Project-Wide
334- [ ] All templates migrated
335- [ ] All Fluent files complete
336- [ ] Translation key validator passes
337- [ ] HTMX language propagation working
338- [ ] Performance benchmarks improved
339- [ ] Documentation updated
340
341## Common Migration Patterns
342
343### Form Validation Messages
344```html
345<!-- Pattern for validation errors -->
346{% if field_errors %}
347 <div class="field-errors">
348 {% for error in field_errors %}
349 <span class="error">
350 {{ t(key=error.translation_key, locale=locale, args=error.args) }}
351 </span>
352 {% endfor %}
353 </div>
354{% endif %}
355```
356
357### Conditional Gender Messages
358```html
359<!-- Pattern for conditional gender content -->
360{% if user_gender == "feminine" %}
361 {{ tg(key="welcome-message", locale=locale, gender="feminine") }}
362{% elif user_gender == "masculine" %}
363 {{ tg(key="welcome-message", locale=locale, gender="masculine") }}
364{% else %}
365 {{ tg(key="welcome-message", locale=locale, gender="neutral") }}
366{% endif %}
367```
368
369### Count-Based Messages
370```html
371<!-- Pattern for pluralization -->
372<p>{{ tc(key="events-created", locale=locale, count=profile.event_count) }}</p>
373<p>{{ tc(key="followers-count", locale=locale, count=profile.followers) }}</p>
374```
375
376This migration approach ensures a smooth transition from hardcoded strings to a flexible, performance-optimized i18n system while maintaining HTMX compatibility.