i18n+filtering fork - fluent-templates v2

Dev doc I18N_FLUENT_TEMPLATES_GUIDE.md

Changed files
+1428
docs
+1428
docs/I18N_FLUENT_TEMPLATES_GUIDE.md
··· 1 + # Smokesignal i18n with Fluent-Templates: Complete Developer Guide 2 + 3 + **Project**: smokesignal (Event & RSVP Management) 4 + **Date**: June 1, 2025 5 + **Architecture**: fluent-templates with static loading 6 + **Status**: Production Ready ✅ 7 + 8 + --- 9 + 10 + ## Table of Contents 11 + 12 + 1. [Overview](#overview) 13 + 2. [Architecture](#architecture) 14 + 3. [Getting Started](#getting-started) 15 + 4. [Creating and Managing Locales](#creating-and-managing-locales) 16 + 5. [Creating New Templates and Routes](#creating-new-templates-and-routes) 17 + 6. [Template Development](#template-development) 18 + 7. [Debugging and Testing](#debugging-and-testing) 19 + 8. [Best Practices](#best-practices) 20 + 9. [Troubleshooting](#troubleshooting) 21 + 10. [Performance Considerations](#performance-considerations) 22 + 11. [Production Deployment](#production-deployment) 23 + 24 + --- 25 + 26 + ## Overview 27 + 28 + Smokesignal uses **fluent-templates** for internationalization (i18n), providing: 29 + 30 + - **Static Loading**: Zero runtime overhead with compile-time resource loading 31 + - **Automatic Fallbacks**: English fallback for missing translations 32 + - **Gender-Aware**: Full French Canadian gender variant support 33 + - **HTMX Integration**: Smart template selection for partial updates 34 + - **Template Functions**: Direct i18n access in templates via `tr()`, `current_locale()`, etc. 35 + 36 + ### Supported Languages 37 + 38 + - **English (US)**: `en-us` (primary) 39 + - **French Canadian**: `fr-ca` (with gender support) 40 + 41 + ### Key Features 42 + 43 + - ✅ Compile-time translation validation 44 + - ✅ Gender-aware translations (French) 45 + - ✅ HTMX partial template support 46 + - ✅ Automatic template fallbacks 47 + - ✅ Comprehensive debugging tools 48 + - ✅ Professional translation quality 49 + 50 + --- 51 + 52 + ## Architecture 53 + 54 + ### Core Components 55 + 56 + ``` 57 + src/i18n/ 58 + ├── mod.rs # Main i18n exports and compatibility 59 + ├── fluent_loader.rs # Static loader and core functions 60 + ├── template_helpers.rs # Template function integration 61 + ├── gender.rs # Gender context for translations 62 + └── errors.rs # I18n error types 63 + 64 + src/http/ 65 + ├── template_renderer.rs # Unified template rendering system 66 + ├── middleware_i18n.rs # Language detection middleware 67 + └── context.rs # Web context with i18n support 68 + 69 + i18n/ 70 + ├── en-us/ # English translations 71 + │ ├── actions.ftl # Action buttons and commands 72 + │ ├── common.ftl # Common UI elements 73 + │ ├── forms.ftl # Form-related translations 74 + │ ├── ui.ftl # Page-specific UI content 75 + │ └── errors.ftl # Error messages 76 + └── fr-ca/ # French Canadian translations 77 + ├── actions.ftl # (Same structure as English) 78 + ├── common.ftl 79 + ├── forms.ftl 80 + ├── ui.ftl 81 + └── errors.ftl 82 + ``` 83 + 84 + ### Static Loader Configuration 85 + 86 + ```rust 87 + // src/i18n/fluent_loader.rs 88 + static_loader! { 89 + pub static LOCALES = { 90 + locales: "./i18n", // Path to translation files 91 + fallback_language: "en-us", // Default language 92 + customise: |bundle| { 93 + bundle.set_use_isolating(false); // Clean output 94 + }, 95 + }; 96 + } 97 + ``` 98 + 99 + ### Template Integration 100 + 101 + ```rust 102 + // Templates have direct access to i18n functions 103 + {{ tr("welcome-message") }} // Basic translation 104 + {{ tr("user-count", count=users|length) }} // With parameters 105 + {{ tr("welcome-user", gender=user_gender) }} // Gender-aware (French) 106 + {{ current_locale() }} // Current language 107 + {{ has_locale("fr-ca") }} // Language availability check 108 + ``` 109 + 110 + --- 111 + 112 + ## Getting Started 113 + 114 + ### 1. Development Setup 115 + 116 + ```bash 117 + # Build the project (includes i18n validation) 118 + cargo build 119 + 120 + # Run i18n tests 121 + cargo test i18n 122 + 123 + # Run comprehensive i18n testing tool 124 + cargo run --bin i18n-tester 125 + 126 + # Check for FTL duplicate keys 127 + ./utils/find_ftl_duplicates.sh 128 + ``` 129 + 130 + ### 2. Basic Workflow 131 + 132 + 1. **Add Translation Keys**: Update `.ftl` files in both locales 133 + 2. **Update Templates**: Use `tr()` functions in templates 134 + 3. **Test**: Run i18n testing tools 135 + 4. **Build**: Compile to validate all translations 136 + 5. **Deploy**: Static loading means zero runtime overhead 137 + 138 + ### 3. Project Structure 139 + 140 + ``` 141 + smokesignal/ 142 + ├── i18n/ # Translation files 143 + ├── src/i18n/ # I18n system code 144 + ├── templates/ # Jinja2 templates with i18n 145 + ├── utils/ # FTL debugging scripts 146 + └── docs/ # This documentation 147 + ``` 148 + 149 + --- 150 + 151 + ## Creating and Managing Locales 152 + 153 + ### Adding a New Language 154 + 155 + #### Step 1: Create Directory Structure 156 + 157 + ```bash 158 + # Create new locale directory (example: Spanish) 159 + mkdir -p i18n/es-es 160 + 161 + # Copy English files as templates 162 + cp i18n/en-us/*.ftl i18n/es-es/ 163 + ``` 164 + 165 + #### Step 2: Update Supported Languages 166 + 167 + ```rust 168 + // src/i18n/fluent_loader.rs 169 + pub const SUPPORTED_LANGUAGES: &[&str] = &[ 170 + "en-us", 171 + "fr-ca", 172 + "es-es" // Add new language 173 + ]; 174 + ``` 175 + 176 + #### Step 3: Translate Content 177 + 178 + Edit each `.ftl` file in the new locale directory: 179 + 180 + ```fluent 181 + # i18n/es-es/ui.ftl 182 + homepage-title = Señal de Humo 183 + homepage-subtitle = Encuentra eventos, haz conexiones y crea comunidad. 184 + 185 + # With parameters 186 + user-count = { $count -> 187 + [one] { $count } usuario 188 + *[other] { $count } usuarios 189 + } 190 + 191 + # Gender-aware (if needed) 192 + welcome-user = { $gender -> 193 + [masculine] Bienvenido { $name } 194 + [feminine] Bienvenida { $name } 195 + *[other] Bienvenido/a { $name } 196 + } 197 + ``` 198 + 199 + #### Step 4: Create Templates (Optional) 200 + 201 + ```bash 202 + # Create language-specific templates 203 + cp templates/home.en-us.html templates/home.es-es.html 204 + cp templates/home.en-us.partial.html templates/home.es-es.partial.html 205 + cp templates/home.en-us.bare.html templates/home.es-es.bare.html 206 + ``` 207 + 208 + #### Step 5: Test New Locale 209 + 210 + ```bash 211 + # Comprehensive testing 212 + cargo run --bin i18n-tester 213 + 214 + # Check for issues 215 + cargo test test_new_locale_integration 216 + 217 + # Validate no duplicates 218 + ./utils/find_ftl_duplicates.sh 219 + ``` 220 + 221 + ### Managing Gender Support 222 + 223 + For languages requiring gender agreement (like French): 224 + 225 + ```fluent 226 + # Gender-aware translations in fr-ca/ui.ftl 227 + welcome-message = { $gender -> 228 + [masculine] Bienvenue { $name }, vous êtes connecté 229 + [feminine] Bienvenue { $name }, vous êtes connectée 230 + *[other] Bienvenue { $name }, vous êtes connecté·e 231 + } 232 + 233 + status-active = { $gender -> 234 + [masculine] Actif 235 + [feminine] Active 236 + *[other] Actif/Active 237 + } 238 + ``` 239 + 240 + ### File Organization 241 + 242 + #### Translation Key Prefixes 243 + 244 + - `ui-*`: User interface elements (buttons, labels, navigation) 245 + - `form-*`: Form labels, placeholders, validation messages 246 + - `error-*`: Error messages and alerts 247 + - `action-*`: Action buttons and links 248 + - `page-*`: Page titles and headings 249 + - `admin-*`: Admin interface specific 250 + - `event-*`: Event-related content 251 + - `rsvp-*`: RSVP-related content 252 + 253 + #### File Structure 254 + 255 + ``` 256 + actions.ftl # Action buttons (save, edit, delete, etc.) 257 + common.ftl # Common UI elements (navigation, headers) 258 + forms.ftl # Form-related translations 259 + ui.ftl # Page-specific UI content 260 + errors.ftl # Error messages and validation 261 + ``` 262 + 263 + --- 264 + 265 + ## Creating New Templates and Routes 266 + 267 + ### 1. Planning Your New Page 268 + 269 + Before creating templates and routes, plan your page structure: 270 + 271 + ``` 272 + New Page: Event Analytics Dashboard 273 + ├── Route: /analytics/{handle_slug}/{event_rkey} 274 + ├── Handler: handle_analytics_dashboard 275 + ├── Templates: 276 + │ ├── analytics.en-us.html (full page) 277 + │ ├── analytics.en-us.bare.html (HTMX boosted) 278 + │ ├── analytics.en-us.partial.html (HTMX partial) 279 + │ ├── analytics.fr-ca.html (French full page) 280 + │ ├── analytics.fr-ca.bare.html (French boosted) 281 + │ └── analytics.fr-ca.partial.html (French partial) 282 + └── Translation Keys: 283 + ├── page-title-analytics 284 + ├── analytics-* 285 + └── chart-* 286 + ``` 287 + 288 + ### 2. Add Translation Keys 289 + 290 + First, add all required translation keys to both locales: 291 + 292 + ```fluent 293 + # i18n/en-us/ui.ftl 294 + page-title-analytics = Event Analytics Dashboard 295 + analytics-overview = Analytics Overview 296 + analytics-attendees = Attendee Statistics 297 + analytics-timeline = Event Timeline 298 + chart-rsvp-trends = RSVP Trends 299 + chart-attendance-forecast = Attendance Forecast 300 + analytics-export = Export Data 301 + analytics-date-range = Date Range 302 + analytics-no-data = No analytics data available for this event 303 + ``` 304 + 305 + ```fluent 306 + # i18n/fr-ca/ui.ftl 307 + page-title-analytics = Tableau de Bord Analytique 308 + analytics-overview = Aperçu Analytique 309 + analytics-attendees = Statistiques des Participants 310 + analytics-timeline = Chronologie de l'Événement 311 + chart-rsvp-trends = Tendances RSVP 312 + chart-attendance-forecast = Prévision de Participation 313 + analytics-export = Exporter les Données 314 + analytics-date-range = Plage de Dates 315 + analytics-no-data = Aucune donnée analytique disponible pour cet événement 316 + ``` 317 + 318 + ### 3. Create the Route Handler 319 + 320 + ```rust 321 + // src/http/handle_analytics_dashboard.rs 322 + use axum::{ 323 + extract::{Path, State}, 324 + response::Response, 325 + }; 326 + use axum_htmx::HxRequest; 327 + use headers::HeaderMap; 328 + use minijinja::context as template_context; 329 + 330 + use crate::{ 331 + http::{ 332 + context::WebContext, 333 + middleware_i18n::Language, 334 + template_renderer::TemplateRenderer, 335 + }, 336 + i18n::gender::Gender, 337 + }; 338 + 339 + pub async fn handle_analytics_dashboard( 340 + State(web_context): State<WebContext>, 341 + Path((handle_slug, event_rkey)): Path<(String, String)>, 342 + language: Language, 343 + HxRequest(hx_request): HxRequest, 344 + headers: HeaderMap, 345 + ) -> Response { 346 + // Extract HTMX context 347 + let hx_boosted = headers.get("hx-boosted").is_some(); 348 + 349 + // Create renderer with context 350 + let renderer = TemplateRenderer::new( 351 + web_context, 352 + language, 353 + None, // TODO: Extract user gender from session 354 + hx_boosted, 355 + hx_request, 356 + ); 357 + 358 + // TODO: Fetch analytics data from database 359 + let analytics_data = fetch_event_analytics(&handle_slug, &event_rkey).await; 360 + 361 + match analytics_data { 362 + Ok(data) => { 363 + let template_context = template_context! { 364 + handle_slug => handle_slug, 365 + event_rkey => event_rkey, 366 + analytics => data, 367 + chart_data => prepare_chart_data(&data), 368 + }; 369 + 370 + renderer.render_template( 371 + "analytics", 372 + template_context, 373 + Some(&handle_slug), 374 + &format!("/analytics/{}/{}", handle_slug, event_rkey), 375 + ) 376 + } 377 + Err(err) => { 378 + renderer.render_error( 379 + format!("Failed to load analytics: {}", err), 380 + template_context! { 381 + handle_slug => handle_slug, 382 + event_rkey => event_rkey, 383 + } 384 + ) 385 + } 386 + } 387 + } 388 + 389 + async fn fetch_event_analytics(handle_slug: &str, event_rkey: &str) -> Result<AnalyticsData, AnalyticsError> { 390 + // TODO: Implement database queries for analytics 391 + todo!("Implement analytics data fetching") 392 + } 393 + 394 + fn prepare_chart_data(analytics: &AnalyticsData) -> ChartData { 395 + // TODO: Transform analytics data for frontend charts 396 + todo!("Implement chart data preparation") 397 + } 398 + ``` 399 + 400 + ### 4. Add Route to Server 401 + 402 + ```rust 403 + // src/http/server.rs 404 + use crate::http::handle_analytics_dashboard::handle_analytics_dashboard; 405 + 406 + pub fn build_router(web_context: WebContext) -> Router { 407 + Router::new() 408 + // ...existing routes... 409 + .route("/analytics/{handle_slug}/{event_rkey}", get(handle_analytics_dashboard)) 410 + // ...rest of routes... 411 + .with_state(web_context.clone()) 412 + } 413 + ``` 414 + 415 + ### 5. Create Template Files 416 + 417 + #### Full Page Template 418 + 419 + ```html 420 + <!-- templates/analytics.en-us.html --> 421 + {% extends "base.en-us.html" %} 422 + 423 + {% block title %}{{ tr("page-title-analytics") }}{% endblock %} 424 + 425 + {% block content %} 426 + <div class="analytics-dashboard"> 427 + <header class="analytics-header"> 428 + <h1>{{ tr("analytics-overview") }}</h1> 429 + <div class="analytics-controls"> 430 + <select name="date_range" hx-get="/analytics/{{ handle_slug }}/{{ event_rkey }}" 431 + hx-target="#analytics-content" hx-trigger="change"> 432 + <option value="7d">{{ tr("analytics-date-range") }}: 7 {{ tr("common-days") }}</option> 433 + <option value="30d">{{ tr("analytics-date-range") }}: 30 {{ tr("common-days") }}</option> 434 + <option value="all">{{ tr("common-all-time") }}</option> 435 + </select> 436 + <button class="btn-export" hx-get="/analytics/{{ handle_slug }}/{{ event_rkey }}/export"> 437 + {{ tr("analytics-export") }} 438 + </button> 439 + </div> 440 + </header> 441 + 442 + <main id="analytics-content"> 443 + {% if analytics.has_data %} 444 + <section class="analytics-stats"> 445 + <div class="stat-card"> 446 + <h3>{{ tr("analytics-attendees") }}</h3> 447 + <span class="stat-number">{{ analytics.total_rsvps }}</span> 448 + </div> 449 + <div class="stat-card"> 450 + <h3>{{ tr("chart-attendance-forecast") }}</h3> 451 + <span class="stat-number">{{ analytics.projected_attendance }}</span> 452 + </div> 453 + </section> 454 + 455 + <section class="analytics-charts"> 456 + <div class="chart-container"> 457 + <h3>{{ tr("chart-rsvp-trends") }}</h3> 458 + <canvas id="rsvp-trends-chart" data-chart="{{ chart_data.rsvp_trends|tojson }}"></canvas> 459 + </div> 460 + </section> 461 + {% else %} 462 + <div class="no-data-message"> 463 + <p>{{ tr("analytics-no-data") }}</p> 464 + </div> 465 + {% endif %} 466 + </main> 467 + </div> 468 + {% endblock %} 469 + 470 + {% block scripts %} 471 + <script src="/static/js/analytics-charts.js"></script> 472 + {% endblock %} 473 + ``` 474 + 475 + #### HTMX Partial Template 476 + 477 + ```html 478 + <!-- templates/analytics.en-us.partial.html --> 479 + {% if analytics.has_data %} 480 + <section class="analytics-stats"> 481 + <div class="stat-card"> 482 + <h3>{{ tr("analytics-attendees") }}</h3> 483 + <span class="stat-number">{{ analytics.total_rsvps }}</span> 484 + </div> 485 + <div class="stat-card"> 486 + <h3>{{ tr("chart-attendance-forecast") }}</h3> 487 + <span class="stat-number">{{ analytics.projected_attendance }}</span> 488 + </div> 489 + </section> 490 + 491 + <section class="analytics-charts"> 492 + <div class="chart-container"> 493 + <h3>{{ tr("chart-rsvp-trends") }}</h3> 494 + <canvas id="rsvp-trends-chart" data-chart="{{ chart_data.rsvp_trends|tojson }}"></canvas> 495 + </div> 496 + </section> 497 + {% else %} 498 + <div class="no-data-message"> 499 + <p>{{ tr("analytics-no-data") }}</p> 500 + </div> 501 + {% endif %} 502 + ``` 503 + 504 + #### HTMX Bare Template (Boosted Navigation) 505 + 506 + ```html 507 + <!-- templates/analytics.en-us.bare.html --> 508 + <!DOCTYPE html> 509 + <html lang="{{ current_locale() }}"> 510 + <head> 511 + <meta charset="utf-8"> 512 + <title>{{ tr("page-title-analytics") }}</title> 513 + <meta name="viewport" content="width=device-width, initial-scale=1"> 514 + <link rel="stylesheet" href="/static/css/main.css"> 515 + </head> 516 + <body> 517 + {% include "components/analytics-dashboard.en-us.html" %} 518 + <script src="/static/js/analytics-charts.js"></script> 519 + </body> 520 + </html> 521 + ``` 522 + 523 + #### French Templates 524 + 525 + Copy and adapt for French: 526 + 527 + ```bash 528 + # Copy English templates to French 529 + cp templates/analytics.en-us.html templates/analytics.fr-ca.html 530 + cp templates/analytics.en-us.partial.html templates/analytics.fr-ca.partial.html 531 + cp templates/analytics.en-us.bare.html templates/analytics.fr-ca.bare.html 532 + 533 + # The content will be automatically translated via tr() functions 534 + # No manual editing needed unless you need French-specific layouts 535 + ``` 536 + 537 + ### 6. Add Navigation Links 538 + 539 + ```html 540 + <!-- Update navigation templates --> 541 + <!-- templates/components/nav.en-us.html --> 542 + {% if current_user.can_view_analytics %} 543 + <a href="/analytics/{{ current_handle.handle }}/{{ event.rkey }}" 544 + hx-boost="true" 545 + class="nav-link"> 546 + {{ tr("analytics-overview") }} 547 + </a> 548 + {% endif %} 549 + ``` 550 + 551 + ### 7. Test Your New Route 552 + 553 + ```bash 554 + # Test compilation 555 + cargo build 556 + 557 + # Test i18n integration 558 + cargo run --bin i18n-tester 559 + 560 + # Test route (if server running) 561 + curl http://localhost:3000/analytics/testuser/testevent 562 + 563 + # Test HTMX partial 564 + curl -H "HX-Request: true" http://localhost:3000/analytics/testuser/testevent 565 + ``` 566 + 567 + ### 8. Best Practices for New Routes 568 + 569 + #### Template Naming Convention 570 + 571 + ``` 572 + {page_name}.{locale}.{variant}.html 573 + 574 + Examples: 575 + - analytics.en-us.html (Full page) 576 + - analytics.en-us.partial.html (HTMX partial) 577 + - analytics.en-us.bare.html (HTMX boosted) 578 + - analytics.fr-ca.html (French full page) 579 + ``` 580 + 581 + #### Error Handling 582 + 583 + ```rust 584 + // Always provide error context for i18n 585 + match operation_result { 586 + Ok(data) => { 587 + renderer.render_template("analytics", template_context, ...) 588 + } 589 + Err(err) => { 590 + renderer.render_error( 591 + format!("analytics-error: {}", err), 592 + template_context! { 593 + error_context => "analytics", 594 + handle_slug => handle_slug, 595 + } 596 + ) 597 + } 598 + } 599 + ``` 600 + 601 + #### HTMX Integration 602 + 603 + ```rust 604 + // Always extract HTMX context properly 605 + let hx_boosted = headers.get("hx-boosted").is_some(); 606 + let hx_request = hx_request; // From HxRequest extractor 607 + 608 + // Create renderer with proper context 609 + let renderer = TemplateRenderer::new( 610 + web_context, 611 + language, 612 + user_gender, // Extract from session if available 613 + hx_boosted, 614 + hx_request, 615 + ); 616 + ``` 617 + 618 + --- 619 + 620 + ## Template Development 621 + 622 + ### Template Functions Available 623 + 624 + #### Basic Translation 625 + 626 + ```html 627 + <!-- Simple translation --> 628 + <h1>{{ tr("page-title-analytics") }}</h1> 629 + 630 + <!-- With parameters --> 631 + <p>{{ tr("user-count", count=users|length) }}</p> 632 + 633 + <!-- With multiple parameters --> 634 + <span>{{ tr("event-date-range", start=event.start_date, end=event.end_date) }}</span> 635 + ``` 636 + 637 + #### Conditional Content 638 + 639 + ```html 640 + <!-- Language-specific content --> 641 + {% if current_locale() == "fr-ca" %} 642 + <div class="french-specific-content"> 643 + {{ tr("french-cultural-note") }} 644 + </div> 645 + {% endif %} 646 + 647 + <!-- Feature availability --> 648 + {% if has_locale("es-es") %} 649 + <a href="/language/es-es">Español</a> 650 + {% endif %} 651 + ``` 652 + 653 + #### Gender-Aware Content (French) 654 + 655 + ```html 656 + <!-- Pass user gender for French translations --> 657 + <p>{{ tr("welcome-message", gender=user_gender) }}</p> 658 + 659 + <!-- Conditional gender display --> 660 + {% if current_locale() == "fr-ca" and user_gender %} 661 + <span class="gender-specific">{{ tr("status-message", gender=user_gender) }}</span> 662 + {% endif %} 663 + ``` 664 + 665 + ### Template Hierarchy 666 + 667 + #### Full Page (`base.html`) 668 + 669 + ```html 670 + <!DOCTYPE html> 671 + <html lang="{{ current_locale() }}"> 672 + <head> 673 + <meta charset="utf-8"> 674 + <title>{% block title %}{{ tr("site-title") }}{% endblock %}</title> 675 + <meta name="viewport" content="width=device-width, initial-scale=1"> 676 + <link rel="stylesheet" href="/static/css/main.css"> 677 + </head> 678 + <body> 679 + {% include "components/header.html" %} 680 + <main> 681 + {% block content %}{% endblock %} 682 + </main> 683 + {% include "components/footer.html" %} 684 + <script src="/static/js/htmx.min.js"></script> 685 + {% block scripts %}{% endblock %} 686 + </body> 687 + </html> 688 + ``` 689 + 690 + #### HTMX Partial (content only) 691 + 692 + ```html 693 + <!-- No HTML structure, just content --> 694 + <section class="content-update"> 695 + {{ tr("updated-content") }} 696 + <div class="data-display"> 697 + <!-- Updated data here --> 698 + </div> 699 + </section> 700 + ``` 701 + 702 + #### HTMX Bare (minimal HTML) 703 + 704 + ```html 705 + <!DOCTYPE html> 706 + <html lang="{{ current_locale() }}"> 707 + <head> 708 + <meta charset="utf-8"> 709 + <title>{{ tr("page-title") }}</title> 710 + <meta name="viewport" content="width=device-width, initial-scale=1"> 711 + <link rel="stylesheet" href="/static/css/main.css"> 712 + </head> 713 + <body> 714 + {% include "components/content.html" %} 715 + <script src="/static/js/app.js"></script> 716 + </body> 717 + </html> 718 + ``` 719 + 720 + ### Template Fallback System 721 + 722 + The system automatically falls back to English templates if language-specific versions don't exist: 723 + 724 + ``` 725 + Request: analytics.fr-ca.html 726 + 1. Look for: templates/analytics.fr-ca.html 727 + 2. If not found: templates/analytics.en-us.html (fallback) 728 + 3. If not found: Error (base template missing) 729 + ``` 730 + 731 + --- 732 + 733 + ## Debugging and Testing 734 + 735 + ### Built-in Testing Tools 736 + 737 + #### 1. Comprehensive i18n Tester 738 + 739 + ```bash 740 + # Run the full i18n test suite 741 + cargo run --bin i18n-tester 742 + 743 + # This tests: 744 + # - Bundle loading 745 + # - Translation lookups 746 + # - Parameter handling 747 + # - Gender context 748 + # - Missing translations 749 + # - Template functions 750 + # - HTMX integration 751 + # - Language switching 752 + # - Fallback behavior 753 + ``` 754 + 755 + #### 2. Form Validation Tester 756 + 757 + ```bash 758 + # Test form validation i18n integration 759 + cargo run --bin form-validation-tester 760 + 761 + # This tests: 762 + # - Form error translations 763 + # - HTMX partial form updates 764 + # - Language persistence in forms 765 + # - Context-aware error messages 766 + ``` 767 + 768 + #### 3. FTL Duplicate Detection 769 + 770 + ```bash 771 + # Check for duplicate translation keys 772 + ./utils/find_ftl_duplicates.sh 773 + 774 + # Clean up duplicates (interactive) 775 + ./utils/clean_ftl_duplicates.sh 776 + ``` 777 + 778 + ### Debugging Commands 779 + 780 + #### Validate Bundle Loading 781 + 782 + ```rust 783 + // Add to your application startup 784 + use smokesignal::i18n::{validate_bundle_loading, check_for_duplicate_issues}; 785 + 786 + fn main() { 787 + // Initialize tracing first 788 + tracing_subscriber::init(); 789 + 790 + // Validate i18n system 791 + validate_bundle_loading(); 792 + check_for_duplicate_issues(); 793 + 794 + // Continue with app startup... 795 + } 796 + ``` 797 + 798 + #### Test Individual Translations 799 + 800 + ```rust 801 + use smokesignal::i18n::{get_translation, LOCALES}; 802 + use unic_langid::langid; 803 + 804 + // Test a specific translation 805 + let en_locale = langid!("en-us"); 806 + let result = get_translation(&en_locale, "welcome-message", None); 807 + println!("Translation: {}", result); 808 + 809 + // Test with parameters 810 + let mut args = HashMap::new(); 811 + args.insert("name".to_string(), FluentValue::from("Alice")); 812 + let result = get_translation(&en_locale, "welcome-user", Some(args)); 813 + ``` 814 + 815 + ### Common Debugging Scenarios 816 + 817 + #### Missing Translation 818 + 819 + **Symptoms**: Key name displayed instead of translation 820 + ``` 821 + Output: "welcome-message" instead of "Welcome!" 822 + ``` 823 + 824 + **Solution**: 825 + 1. Check if key exists in `.ftl` files 826 + 2. Verify key spelling matches exactly 827 + 3. Run duplicate detection tools 828 + 4. Check bundle loading logs 829 + 830 + #### Gender Context Not Working 831 + 832 + **Symptoms**: Generic translation instead of gendered version 833 + ``` 834 + Expected: "Bienvenue Marie, vous êtes connectée" (feminine) 835 + Actual: "Bienvenue Marie, vous êtes connecté" (default) 836 + ``` 837 + 838 + **Solution**: 839 + 1. Verify gender parameter is passed correctly 840 + 2. Check French `.ftl` file has gender variants 841 + 3. Ensure gender context reaches template 842 + 843 + #### HTMX Language Switching 844 + 845 + **Symptoms**: HTMX requests don't maintain language 846 + ``` 847 + Page loads in French, HTMX updates appear in English 848 + ``` 849 + 850 + **Solution**: 851 + 1. Verify `HX-Current-Language` header is set 852 + 2. Check middleware_i18n language detection 853 + 3. Ensure templates use proper `hx-headers` attributes 854 + 855 + ### Testing Checklist 856 + 857 + Before deploying new i18n features: 858 + 859 + - [ ] All translation keys exist in both languages 860 + - [ ] No duplicate keys detected 861 + - [ ] i18n-tester passes all tests 862 + - [ ] Form validation works in both languages 863 + - [ ] HTMX partial updates maintain language 864 + - [ ] Gender context works for French 865 + - [ ] Template fallbacks work correctly 866 + - [ ] Error messages display in correct language 867 + - [ ] Language switching preserves page state 868 + 869 + --- 870 + 871 + ## Best Practices 872 + 873 + ### Translation Key Naming 874 + 875 + #### Use Clear, Hierarchical Prefixes 876 + 877 + ```fluent 878 + # Good 879 + page-title-analytics = Analytics Dashboard 880 + form-validation-email = Please enter a valid email address 881 + error-event-not-found = Event not found 882 + 883 + # Avoid 884 + analytics = Analytics Dashboard 885 + email = Please enter a valid email address 886 + not-found = Event not found 887 + ``` 888 + 889 + #### Keep Keys Short but Descriptive 890 + 891 + ```fluent 892 + # Good 893 + btn-save = Save 894 + btn-cancel = Cancel 895 + modal-confirm-delete = Are you sure you want to delete this item? 896 + 897 + # Too verbose 898 + button-that-saves-the-current-form = Save 899 + button-that-cancels-the-current-operation = Cancel 900 + ``` 901 + 902 + ### Parameter Usage 903 + 904 + #### Use Descriptive Parameter Names 905 + 906 + ```fluent 907 + # Good 908 + user-greeting = Hello { $username }, you have { $message_count } new messages 909 + event-capacity = { $current_attendees } of { $max_capacity } attendees 910 + 911 + # Avoid generic names 912 + user-greeting = Hello { $a }, you have { $b } new messages 913 + ``` 914 + 915 + #### Handle Pluralization 916 + 917 + ```fluent 918 + # English pluralization 919 + message-count = { $count -> 920 + [one] { $count } message 921 + *[other] { $count } messages 922 + } 923 + 924 + # French pluralization (more complex) 925 + message-count = { $count -> 926 + [zero] Aucun message 927 + [one] { $count } message 928 + *[other] { $count } messages 929 + } 930 + ``` 931 + 932 + ### Gender-Aware Translations 933 + 934 + #### French Gender Variants 935 + 936 + ```fluent 937 + # User status with gender agreement 938 + user-status-active = { $gender -> 939 + [masculine] Vous êtes actif 940 + [feminine] Vous êtes active 941 + *[other] Vous êtes actif·ve 942 + } 943 + 944 + # Past participles 945 + action-completed = { $gender -> 946 + [masculine] { $action } terminé 947 + [feminine] { $action } terminée 948 + *[other] { $action } terminé·e 949 + } 950 + ``` 951 + 952 + #### Fallback Strategy 953 + 954 + ```fluent 955 + # Always provide a neutral fallback 956 + welcome-message = { $gender -> 957 + [masculine] Bienvenue { $name } 958 + [feminine] Bienvenue { $name } 959 + *[other] Bienvenue { $name } 960 + } 961 + ``` 962 + 963 + ### Template Best Practices 964 + 965 + #### Consistent Template Structure 966 + 967 + ```html 968 + <!-- Always use the same template structure --> 969 + {% extends "base.html" %} 970 + 971 + {% block title %}{{ tr("page-title-specific") }}{% endblock %} 972 + 973 + {% block content %} 974 + <main class="page-specific"> 975 + <header> 976 + <h1>{{ tr("page-heading") }}</h1> 977 + </header> 978 + <!-- Content here --> 979 + </main> 980 + {% endblock %} 981 + ``` 982 + 983 + #### HTMX Integration 984 + 985 + ```html 986 + <!-- Always include language in HTMX headers --> 987 + <div hx-get="/api/data" 988 + hx-headers='{"HX-Current-Language": "{{ current_locale() }}"}'> 989 + {{ tr("loading-placeholder") }} 990 + </div> 991 + 992 + <!-- Use proper targeting --> 993 + <form hx-post="/submit" 994 + hx-target="#form-result" 995 + hx-headers='{"HX-Current-Language": "{{ current_locale() }}"}'> 996 + <!-- Form fields --> 997 + </form> 998 + ``` 999 + 1000 + #### Error Handling 1001 + 1002 + ```html 1003 + <!-- Consistent error display --> 1004 + {% if error_message %} 1005 + <div class="alert alert-error"> 1006 + {{ tr("error-prefix") }}: {{ error_message }} 1007 + </div> 1008 + {% endif %} 1009 + ``` 1010 + 1011 + ### Performance Optimization 1012 + 1013 + #### Minimize Template Variants 1014 + 1015 + ``` 1016 + # Instead of creating many language-specific templates: 1017 + templates/ 1018 + ├── page.en-us.html 1019 + ├── page.fr-ca.html 1020 + ├── page.es-es.html 1021 + └── ... 1022 + 1023 + # Use one template with i18n functions: 1024 + templates/ 1025 + └── page.html (uses tr() functions) 1026 + ``` 1027 + 1028 + #### Static Resource References 1029 + 1030 + ```html 1031 + <!-- Use locale in static resource paths if needed --> 1032 + <link rel="stylesheet" href="/static/css/{{ current_locale() }}/custom.css"> 1033 + 1034 + <!-- Or use conditional loading --> 1035 + {% if current_locale() == "fr-ca" %} 1036 + <link rel="stylesheet" href="/static/css/french-typography.css"> 1037 + {% endif %} 1038 + ``` 1039 + 1040 + --- 1041 + 1042 + ## Troubleshooting 1043 + 1044 + ### Common Issues and Solutions 1045 + 1046 + #### 1. "Translation key not found" Errors 1047 + 1048 + **Problem**: Keys display as-is instead of translations 1049 + 1050 + **Diagnosis**: 1051 + ```bash 1052 + # Check if key exists 1053 + grep -r "your-key-name" i18n/ 1054 + 1055 + # Run duplicate checker 1056 + ./utils/find_ftl_duplicates.sh 1057 + 1058 + # Validate bundle loading 1059 + cargo run --bin i18n-tester 1060 + ``` 1061 + 1062 + **Solutions**: 1063 + - Add missing keys to `.ftl` files 1064 + - Fix typos in key names 1065 + - Remove duplicate keys 1066 + - Verify bundle loading succeeds 1067 + 1068 + #### 2. Gender Context Not Working 1069 + 1070 + **Problem**: French translations don't use correct gender 1071 + 1072 + **Diagnosis**: 1073 + ```rust 1074 + // Add debug logging in template 1075 + {{ tr("debug-gender", gender=user_gender) }} 1076 + 1077 + // Check gender parameter in handler 1078 + println!("User gender: {:?}", user_gender); 1079 + ``` 1080 + 1081 + **Solutions**: 1082 + - Ensure gender is extracted from user session 1083 + - Pass gender parameter to TemplateRenderer 1084 + - Verify French `.ftl` has gender variants 1085 + - Check gender parameter name matches template 1086 + 1087 + #### 3. HTMX Language Switching Issues 1088 + 1089 + **Problem**: HTMX requests lose language context 1090 + 1091 + **Diagnosis**: 1092 + ```bash 1093 + # Check request headers 1094 + curl -H "HX-Request: true" -H "HX-Current-Language: fr-ca" \ 1095 + http://localhost:3000/your-endpoint 1096 + ``` 1097 + 1098 + **Solutions**: 1099 + - Add `HX-Current-Language` header to HTMX requests 1100 + - Verify middleware_i18n processes header correctly 1101 + - Check template renderer receives correct language 1102 + - Ensure HTMX templates include language context 1103 + 1104 + #### 4. Template Fallback Not Working 1105 + 1106 + **Problem**: Missing language templates cause errors 1107 + 1108 + **Diagnosis**: 1109 + ```bash 1110 + # Check template file existence 1111 + ls templates/your-template.*.html 1112 + 1113 + # Test fallback logic 1114 + cargo test test_template_fallback 1115 + ``` 1116 + 1117 + **Solutions**: 1118 + - Ensure English base templates exist 1119 + - Check template naming convention 1120 + - Verify fallback logic in template selection 1121 + - Create missing base templates 1122 + 1123 + #### 5. Build-Time Bundle Loading Errors 1124 + 1125 + **Problem**: Compilation fails due to i18n issues 1126 + 1127 + **Diagnosis**: 1128 + ```bash 1129 + # Build with verbose output 1130 + cargo build --verbose 1131 + 1132 + # Check for FTL syntax errors 1133 + ./utils/find_ftl_duplicates.sh 1134 + ``` 1135 + 1136 + **Solutions**: 1137 + - Fix FTL syntax errors 1138 + - Remove duplicate keys 1139 + - Verify all referenced locales exist 1140 + - Check static loader configuration 1141 + 1142 + ### Debug Tools and Commands 1143 + 1144 + #### Enable Debug Logging 1145 + 1146 + ```rust 1147 + // Add to main.rs or test files 1148 + use tracing_subscriber::{FmtSubscriber, EnvFilter}; 1149 + 1150 + let subscriber = FmtSubscriber::builder() 1151 + .with_env_filter(EnvFilter::from_default_env().add_directive("debug".parse()?)) 1152 + .finish(); 1153 + tracing::subscriber::set_global_default(subscriber)?; 1154 + ``` 1155 + 1156 + #### Test Specific Translations 1157 + 1158 + ```bash 1159 + # Test individual keys 1160 + cargo run --bin i18n-tester | grep "your-key-name" 1161 + 1162 + # Test specific locale 1163 + RUST_LOG=debug cargo run --bin i18n-tester 1164 + ``` 1165 + 1166 + #### Validate Bundle Health 1167 + 1168 + ```bash 1169 + # Comprehensive validation 1170 + cargo run --bin i18n-tester 1171 + 1172 + # Check for duplicates 1173 + ./utils/find_ftl_duplicates.sh 1174 + 1175 + # Manual bundle inspection 1176 + find i18n/ -name "*.ftl" -exec echo "=== {} ===" \; -exec cat {} \; 1177 + ``` 1178 + 1179 + --- 1180 + 1181 + ## Performance Considerations 1182 + 1183 + ### Static Loading Benefits 1184 + 1185 + The fluent-templates static loader provides: 1186 + 1187 + - **Zero Runtime Cost**: All translations loaded at compile time 1188 + - **No File I/O**: No disk reads during request handling 1189 + - **Memory Efficient**: Optimized bundle storage 1190 + - **Fast Lookups**: O(1) translation lookups 1191 + 1192 + ### Bundle Size Optimization 1193 + 1194 + #### Keep Translation Files Focused 1195 + 1196 + ```fluent 1197 + # Instead of one large file: 1198 + # ui.ftl (2000+ lines) 1199 + 1200 + # Use multiple focused files: 1201 + # actions.ftl (50 lines) 1202 + # forms.ftl (100 lines) 1203 + # errors.ftl (75 lines) 1204 + ``` 1205 + 1206 + #### Remove Unused Translations 1207 + 1208 + ```bash 1209 + # Find potentially unused keys 1210 + grep -r "tr(" templates/ | grep -o "tr([\"'][^\"']*[\"'])" | sort | uniq > used_keys.txt 1211 + find i18n/ -name "*.ftl" -exec grep -o "^[a-zA-Z][a-zA-Z0-9_-]*" {} \; | sort | uniq > all_keys.txt 1212 + comm -23 all_keys.txt used_keys.txt # Keys in FTL but not used in templates 1213 + ``` 1214 + 1215 + ### Template Performance 1216 + 1217 + #### Minimize Template Variants 1218 + 1219 + Use i18n functions instead of duplicate templates: 1220 + 1221 + ```html 1222 + <!-- Instead of separate templates per language --> 1223 + <!-- Good: One template with i18n --> 1224 + <h1>{{ tr("page-title") }}</h1> 1225 + <p>{{ tr("page-description") }}</p> 1226 + 1227 + <!-- Avoid: Separate templates --> 1228 + <!-- page.en-us.html: <h1>Dashboard</h1> --> 1229 + <!-- page.fr-ca.html: <h1>Tableau de Bord</h1> --> 1230 + ``` 1231 + 1232 + #### Cache Template Compilation 1233 + 1234 + The TemplateRenderer automatically handles template caching: 1235 + 1236 + ```rust 1237 + // Templates are compiled once and cached 1238 + let renderer = TemplateRenderer::new(web_context, language, gender, hx_boosted, hx_request); 1239 + // Subsequent renders use cached compiled templates 1240 + ``` 1241 + 1242 + ### HTMX Optimization 1243 + 1244 + #### Minimize Partial Template Size 1245 + 1246 + ```html 1247 + <!-- Good: Small, focused partials --> 1248 + <div id="user-count">{{ tr("user-count", count=users|length) }}</div> 1249 + 1250 + <!-- Avoid: Large partials that re-render everything --> 1251 + <div id="entire-dashboard"> 1252 + <!-- Hundreds of lines --> 1253 + </div> 1254 + ``` 1255 + 1256 + #### Language Header Efficiency 1257 + 1258 + ```html 1259 + <!-- Set once, reuse for all HTMX requests --> 1260 + <div hx-headers='{"HX-Current-Language": "{{ current_locale() }}"}'> 1261 + <!-- All child HTMX requests inherit language --> 1262 + </div> 1263 + ``` 1264 + 1265 + --- 1266 + 1267 + ## Production Deployment 1268 + 1269 + ### Pre-Deployment Checklist 1270 + 1271 + #### 1. Translation Completeness 1272 + 1273 + ```bash 1274 + # Verify all keys exist in all locales 1275 + cargo run --bin i18n-tester | grep -i "missing\|error" 1276 + 1277 + # Check for duplicate keys 1278 + ./utils/find_ftl_duplicates.sh 1279 + ``` 1280 + 1281 + #### 2. Template Coverage 1282 + 1283 + ```bash 1284 + # Verify all required templates exist 1285 + find templates/ -name "*.en-us.html" | while read f; do 1286 + base=$(basename "$f" .en-us.html) 1287 + if [ ! -f "templates/${base}.fr-ca.html" ]; then 1288 + echo "Missing French template: ${base}.fr-ca.html" 1289 + fi 1290 + done 1291 + ``` 1292 + 1293 + #### 3. Build Validation 1294 + 1295 + ```bash 1296 + # Clean build to verify all i18n resources 1297 + cargo clean 1298 + cargo build --release 1299 + 1300 + # Run all tests 1301 + cargo test 1302 + ``` 1303 + 1304 + #### 4. Performance Testing 1305 + 1306 + ```bash 1307 + # Bundle size check 1308 + du -sh target/release/smokesignal 1309 + 1310 + # Translation lookup benchmarks 1311 + cargo bench i18n_benchmarks 1312 + ``` 1313 + 1314 + ### Production Configuration 1315 + 1316 + #### Environment Variables 1317 + 1318 + ```bash 1319 + # Set appropriate log level 1320 + export RUST_LOG=warn,smokesignal=info 1321 + 1322 + # Production optimizations 1323 + export CARGO_PROFILE_RELEASE_LTO=true 1324 + export CARGO_PROFILE_RELEASE_STRIP=true 1325 + ``` 1326 + 1327 + #### Docker Deployment 1328 + 1329 + ```dockerfile 1330 + # Dockerfile 1331 + FROM rust:1.83 as builder 1332 + 1333 + WORKDIR /app 1334 + COPY . . 1335 + 1336 + # Build with static i18n resources 1337 + RUN cargo build --release --features embed 1338 + 1339 + FROM debian:bookworm-slim 1340 + COPY --from=builder /app/target/release/smokesignal /usr/local/bin/ 1341 + COPY --from=builder /app/templates /app/templates 1342 + COPY --from=builder /app/static /app/static 1343 + 1344 + # Note: i18n files are embedded in binary, no need to copy 1345 + 1346 + CMD ["smokesignal"] 1347 + ``` 1348 + 1349 + ### Monitoring and Observability 1350 + 1351 + #### Add i18n Metrics 1352 + 1353 + ```rust 1354 + // Add to your metrics collection 1355 + use metrics::{counter, histogram}; 1356 + 1357 + // Track translation lookups 1358 + counter!("i18n.lookups", 1, "locale" => locale.to_string()); 1359 + 1360 + // Track template rendering 1361 + histogram!("template.render_duration", duration.as_millis() as f64, 1362 + "template" => template_name, "locale" => locale.to_string()); 1363 + ``` 1364 + 1365 + #### Health Checks 1366 + 1367 + ```rust 1368 + // Add i18n health check endpoint 1369 + pub async fn handle_health_check() -> Response { 1370 + // Verify key translations work 1371 + let test_translation = get_translation(&langid!("en-us"), "ui-welcome", None); 1372 + 1373 + if test_translation == "ui-welcome" { 1374 + // Translation failed 1375 + (StatusCode::SERVICE_UNAVAILABLE, "i18n system not healthy").into_response() 1376 + } else { 1377 + (StatusCode::OK, "healthy").into_response() 1378 + } 1379 + } 1380 + ``` 1381 + 1382 + ### Maintenance 1383 + 1384 + #### Regular Tasks 1385 + 1386 + 1. **Translation Updates**: Review and update translations quarterly 1387 + 2. **Key Cleanup**: Remove unused translation keys monthly 1388 + 3. **Duplicate Detection**: Run duplicate checker before each release 1389 + 4. **Performance Review**: Monitor translation lookup performance 1390 + 1391 + #### Backup and Recovery 1392 + 1393 + ```bash 1394 + # Backup translation files 1395 + tar -czf i18n-backup-$(date +%Y%m%d).tar.gz i18n/ 1396 + 1397 + # Version control all translation changes 1398 + git add i18n/ 1399 + git commit -m "Update translations for release v1.2.3" 1400 + ``` 1401 + 1402 + --- 1403 + 1404 + ## Conclusion 1405 + 1406 + This guide covers the complete lifecycle of i18n development with fluent-templates in smokesignal. The system provides: 1407 + 1408 + - **Developer-Friendly**: Easy template integration with `tr()` functions 1409 + - **Performance-Optimized**: Static loading with zero runtime overhead 1410 + - **Production-Ready**: Comprehensive testing and debugging tools 1411 + - **Scalable**: Easy addition of new languages and locales 1412 + - **HTMX-Compatible**: Seamless integration with dynamic updates 1413 + 1414 + For additional support or questions, refer to: 1415 + 1416 + - [fluent-templates documentation](https://github.com/XAMPPRocky/fluent-templates) 1417 + - [Fluent syntax guide](https://projectfluent.org/fluent/guide/) 1418 + - [MiniJinja template documentation](https://docs.rs/minijinja/latest/minijinja/) 1419 + 1420 + --- 1421 + 1422 + **Next Steps**: 1423 + 1. Run `cargo run --bin i18n-tester` to validate your setup 1424 + 2. Create your first new locale following the guide above 1425 + 3. Develop templates using the best practices outlined 1426 + 4. Deploy with confidence using the production checklist 1427 + 1428 + Happy internationalizing! 🌐