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.