forked from
smokesignal.events/smokesignal
Fork i18n + search + filtering- v0.2
Smokesignal Template Migration Guidelines#
This document provides step-by-step guidance for migrating existing hardcoded templates to use the new i18n system with on-demand translation functions.
Migration Overview#
Migrate from hardcoded strings in templates to Fluent-based translations using template functions. This migration eliminates pre-rendered translation HashMaps and improves HTMX performance.
Migration Strategy#
Phase 1: Template Analysis & Key Extraction#
1.1 Inventory Existing Strings#
# Find all hardcoded strings in templates
find templates/ -name "*.html" -exec grep -Hn '"[^"]*"' {} \; > strings_inventory.txt
find templates/ -name "*.html" -exec grep -Hn "'[^']*'" {} \; >> strings_inventory.txt
# Categorize by domain for organized migration
grep -E "(button|btn|submit|save|edit|delete|cancel)" strings_inventory.txt > actions.txt
grep -E "(error|fail|invalid|required)" strings_inventory.txt > errors.txt
grep -E "(title|heading|h1|h2|h3)" strings_inventory.txt > headings.txt
grep -E "(label|placeholder|hint)" strings_inventory.txt > forms.txt
1.2 Create Translation Key Naming Convention#
# Pattern: domain-purpose[-variant]
save-changes # Basic action
edit-profile # Specific action
validation-required # Error message
profile-title # Page heading
enter-name-placeholder # Form guidance
welcome-message-feminine # Gender variant
Phase 2: Fluent File Creation#
2.1 Organize by Category#
# i18n/en-us/actions.ftl
save-changes = Save Changes
edit-profile = Edit Profile
delete-item = Delete
cancel-action = Cancel
follow-user = Follow
unfollow-user = Unfollow
# i18n/en-us/errors.ftl
validation-required = This field is required
validation-email = Please enter a valid email
validation-minlength = Must be at least {$min} characters
form-submit-error = Unable to submit form
profile-not-found = Profile not found
# i18n/en-us/ui.ftl
profile-title = Profile
member-since = Member since
events-created = Events Created
welcome-message = Welcome
search-placeholder = Search...
# i18n/fr-ca/actions.ftl
save-changes = Enregistrer les modifications
edit-profile = Modifier le profil
delete-item = Supprimer
cancel-action = Annuler
follow-user = Suivre
unfollow-user = Ne plus suivre
2.2 Gender-Aware Translations#
# English (gender-neutral by default)
welcome-message = Welcome
profile-greeting = Hello there
# French Canadian (gender variants)
welcome-message = Bienvenue
welcome-message-feminine = Bienvenue
welcome-message-masculine = Bienvenu
welcome-message-neutral = Bienvenue
profile-greeting = Bonjour
profile-greeting-feminine = Bonjour madame
profile-greeting-masculine = Bonjour monsieur
profile-greeting-neutral = Bonjour
Phase 3: Template Function Integration#
3.1 Replace Simple Strings#
<!-- Before -->
<button class="button">Save Changes</button>
<h1>Profile</h1>
<p>Member since {{ profile.created_at }}</p>
<!-- After -->
<button class="button">{{ t(key="save-changes", locale=locale) }}</button>
<h1>{{ t(key="profile-title", locale=locale) }}</h1>
<p>{{ t(key="member-since", locale=locale) }} {{ profile.created_at }}</p>
3.2 Add Gender-Aware Translations#
<!-- Before -->
<h2>Welcome, {{ user.name }}!</h2>
<!-- After -->
<h2>{{ tg(key="welcome-message", locale=locale, gender=user_gender) }}, {{ user.name }}!</h2>
3.3 Handle Parameterized Messages#
<!-- Before -->
<p>You have {{ event_count }} events</p>
<!-- After -->
<p>{{ tc(key="events-count", locale=locale, count=event_count) }}</p>
Phase 4: HTMX-Specific Migration#
4.1 Form Templates with Language Propagation#
<!-- Before -->
<form hx-post="/profile/update" hx-target="#profile-content">
<label>Display Name</label>
<input name="display_name" placeholder="Enter your name" />
<button type="submit">Save Changes</button>
</form>
<!-- After -->
<form hx-post="/profile/update"
hx-target="#profile-content"
hx-headers='{"HX-Current-Language": "{{ locale }}"}'>
<label>{{ t(key="display-name", locale=locale) }}</label>
<input name="display_name"
placeholder="{{ t(key="enter-name-placeholder", locale=locale) }}" />
<button type="submit">{{ t(key="save-changes", locale=locale) }}</button>
</form>
4.2 Error Message Templates#
<!-- Before -->
<div class="error">Invalid email address</div>
<!-- After -->
<div class="error">{{ t(key="validation-email", locale=locale) }}</div>
Phase 5: Template Hierarchy Migration#
5.1 Base Template Updates#
<!-- templates/base.en-us.html -->
<!doctype html>
<html lang="{{ language }}">
<head>
<title>{{ t(key="site-title", locale=locale) }}</title>
<meta name="description" content="{{ t(key="site-description", locale=locale) }}">
</head>
<body data-current-language="{{ locale }}">
{% include 'nav.html' %}
{% block content %}{% endblock %}
{% include 'footer.html' %}
</body>
</html>
5.2 Partial Templates for HTMX#
<!-- templates/partials/profile_form.html -->
<div id="profile-form" data-current-language="{{ locale }}">
<h3>{{ t(key="edit-profile-title", locale=locale) }}</h3>
{% if errors %}
<div class="errors">
{% for error in errors %}
<p class="error">{{ t(key=error.key, locale=locale) }}</p>
{% endfor %}
</div>
{% endif %}
<form hx-post="/profile/update"
hx-target="#profile-content"
hx-headers='{"HX-Current-Language": "{{ locale }}"}'>
<!-- Form fields with translations -->
</form>
</div>
Migration Tools & Automation#
Automated String Replacement Script#
#!/bin/bash
# migrate_template.sh
TEMPLATE_FILE=$1
BACKUP_FILE="${TEMPLATE_FILE}.bak"
# Create backup
cp "$TEMPLATE_FILE" "$BACKUP_FILE"
# Replace common patterns
sed -i 's/"Save Changes"/{{ t(key="save-changes", locale=locale) }}/g' "$TEMPLATE_FILE"
sed -i 's/"Edit Profile"/{{ t(key="edit-profile", locale=locale) }}/g' "$TEMPLATE_FILE"
sed -i 's/"Delete"/{{ t(key="delete-item", locale=locale) }}/g' "$TEMPLATE_FILE"
sed -i 's/"Cancel"/{{ t(key="cancel-action", locale=locale) }}/g' "$TEMPLATE_FILE"
# Handle form labels
sed -i 's/"Display Name"/{{ t(key="display-name", locale=locale) }}/g' "$TEMPLATE_FILE"
sed -i 's/"Email"/{{ t(key="email", locale=locale) }}/g' "$TEMPLATE_FILE"
echo "Migrated $TEMPLATE_FILE (backup: $BACKUP_FILE)"
Translation Key Validator#
// tools/validate_keys.rs
use std::collections::HashSet;
use regex::Regex;
fn extract_translation_keys_from_templates() -> HashSet<String> {
let re = Regex::new(r#"\{\{\s*t\w*\(key="([^"]+)""#).unwrap();
// Extract all translation keys from templates
// Return set of used keys
}
fn load_fluent_keys() -> HashSet<String> {
// Load all keys from .ftl files
// Return set of available keys
}
#[test]
fn test_all_translation_keys_exist() {
let used_keys = extract_translation_keys_from_templates();
let available_keys = load_fluent_keys();
for key in &used_keys {
assert!(
available_keys.contains(key),
"Missing translation key: {} (used in templates)",
key
);
}
println!("✅ All {} translation keys validated", used_keys.len());
}
Migration Validation#
Template Syntax Validation#
# Validate template syntax after migration
find templates/ -name "*.html" -exec python3 -c "
import sys
import re
def validate_template(file_path):
with open(file_path, 'r') as f:
content = f.read()
# Check for proper function calls
pattern = r'\{\{\s*t[gc]?\(key=[\"'\''][^\"\']+[\"'\''][^}]*\)\s*\}\}'
matches = re.findall(pattern, content)
# Check for missing locale parameter
missing_locale = re.findall(r'\{\{\s*t[gc]?\([^}]*\)\s*\}\}', content)
print(f'File: {file_path}')
print(f' Translation calls: {len(matches)}')
if missing_locale:
print(f' ⚠️ Potential missing locale: {len(missing_locale)}')
validate_template(sys.argv[1])
" {} \;
Performance Comparison#
// Compare before/after performance
#[cfg(test)]
mod migration_performance_tests {
#[test]
fn benchmark_old_vs_new_rendering() {
// Test pre-rendered HashMap approach vs on-demand functions
let start = std::time::Instant::now();
// Old approach: pre-render all translations
let _old_result = render_with_prerendered_translations();
let old_duration = start.elapsed();
let start = std::time::Instant::now();
// New approach: on-demand translation functions
let _new_result = render_with_template_functions();
let new_duration = start.elapsed();
println!("Old approach: {:?}", old_duration);
println!("New approach: {:?}", new_duration);
// Expect significant improvement
assert!(new_duration < old_duration * 3 / 4);
}
}
Migration Checklist#
Per Template#
- Backup original template
- Extract all hardcoded strings
- Create corresponding Fluent keys
- Replace strings with template functions
- Add HTMX language headers if applicable
- Test rendering in both languages
- Validate gender variants (if applicable)
- Performance test HTMX interactions
Per Handler#
- Remove pre-rendered translation HashMap
- Use minimal template context
- Ensure Language extractor is used
- Add proper error handling for missing keys
- Test with HTMX requests
Project-Wide#
- All templates migrated
- All Fluent files complete
- Translation key validator passes
- HTMX language propagation working
- Performance benchmarks improved
- Documentation updated
Common Migration Patterns#
Form Validation Messages#
<!-- Pattern for validation errors -->
{% if field_errors %}
<div class="field-errors">
{% for error in field_errors %}
<span class="error">
{{ t(key=error.translation_key, locale=locale, args=error.args) }}
</span>
{% endfor %}
</div>
{% endif %}
Conditional Gender Messages#
<!-- Pattern for conditional gender content -->
{% if user_gender == "feminine" %}
{{ tg(key="welcome-message", locale=locale, gender="feminine") }}
{% elif user_gender == "masculine" %}
{{ tg(key="welcome-message", locale=locale, gender="masculine") }}
{% else %}
{{ tg(key="welcome-message", locale=locale, gender="neutral") }}
{% endif %}
Count-Based Messages#
<!-- Pattern for pluralization -->
<p>{{ tc(key="events-created", locale=locale, count=profile.event_count) }}</p>
<p>{{ tc(key="followers-count", locale=locale, count=profile.followers) }}</p>
This migration approach ensures a smooth transition from hardcoded strings to a flexible, performance-optimized i18n system while maintaining HTMX compatibility.