+1428
docs/I18N_FLUENT_TEMPLATES_GUIDE.md
+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! 🌐