diff --git a/docs/i18n_phase2_complete.md b/docs/PHASE1-2_COMPLETION.md similarity index 100% rename from docs/i18n_phase2_complete.md rename to docs/PHASE1-2_COMPLETION.md diff --git a/docs/i18n_phase2_integration.md b/docs/PHASE1-2_FIRSTPASS.md similarity index 100% rename from docs/i18n_phase2_integration.md rename to docs/PHASE1-2_FIRSTPASS.md diff --git a/docs/HTMX_I18N_INTEGRATION.md b/docs/PHASE3_COMPLETION.md similarity index 100% rename from docs/HTMX_I18N_INTEGRATION.md rename to docs/PHASE3_COMPLETION.md diff --git a/docs/filtering_module.md b/docs/filtering_module.md new file mode 100644 index 0000000..16c82eb --- /dev/null +++ b/docs/filtering_module.md @@ -0,0 +1,1004 @@ +# Smokesignal Event Filtering Module - Technical Summary + +## Project Context + +This document summarizes the design and implementation approach for a new event filtering module in the Smokesignal application, a Rust-based social platform built on ATproto. The module provides faceted search and filtering capabilities for events while integrating with the existing i18n and caching infrastructure. + +## Core Requirements + +1. **Filtering Capabilities**: Support filtering events by multiple criteria including text search, dates, categories, and geolocation +2. **Faceted Navigation**: Display available filtering options with counts for each facet value +3. **HTMX Integration**: Support partial page updates with stateful filtering +4. **I18n Support**: Full internationalization of filters and facets +5. **ATproto Hydration**: Populate events with user profiles and related data +6. **Redis Cache Integration**: Optimize performance using existing cache infrastructure + +## Architecture Overview + +``` +src/filtering/ +├── mod.rs # Exports and FilterContext structure +├── criteria.rs # Filter criteria types +├── query_builder.rs # SQL query construction +├── facets.rs # Facet calculation logic +└── hydration.rs # ATproto entity hydration + +src/http/ +├── middleware_filter.rs # Filter extraction middleware +└── templates_filter.html # HTMX-compatible templates +``` + +## Event Filter Criteria Model + +```rust +#[derive(Debug, Clone, Default, Hash)] +pub struct EventFilterCriteria { + pub search_term: Option, + pub categories: Vec, + pub start_date: Option>, + pub end_date: Option>, + pub location: Option, + pub creator_did: Option, + pub page: usize, + pub page_size: usize, + pub sort_by: EventSortField, + pub sort_order: SortOrder, +} + +#[derive(Debug, Clone)] +pub struct LocationFilter { + pub latitude: f64, + pub longitude: f64, + pub radius_km: f64, +} +``` + +## I18n Integration Requirements + +The filtering module must integrate with the application's existing i18n system: + +1. **Template Functions**: Use direct template functions instead of pre-rendered translations + ```html +

{{ t(key="categories", locale=locale) }}

+ ``` + +2. **Facet Translation**: Support translation of facet values + ```rust + // Create i18n keys for facet values + category.i18n_key = format!("category-{}", category.name.to_lowercase() + .replace(" ", "-").replace("&", "and")); + ``` + +3. **HTMX Language Propagation**: Work with the language middleware + ```html +
+ +
+ ``` + +## QueryBuilder Pattern + +```rust +pub struct EventQueryBuilder { + pool: PgPool, +} + +impl EventQueryBuilder { + pub async fn build_and_execute( + &self, + criteria: &EventFilterCriteria + ) -> Result, FilterError> { + let mut query = sqlx::QueryBuilder::new("SELECT * FROM events WHERE 1=1 "); + + // Apply filters conditionally + if let Some(term) = &criteria.search_term { + query.push(" AND (name ILIKE "); + query.push_bind(format!("%{}%", term)); + query.push(")"); + } + + // Location filtering using PostGIS + if let Some(location) = &criteria.location { + query.push(" AND ST_DWithin( + ST_MakePoint((record->'location'->>'longitude')::float8, + (record->'location'->>'latitude')::float8)::geography, + ST_MakePoint($1, $2)::geography, + $3 + )"); + query.push_bind(location.longitude); + query.push_bind(location.latitude); + query.push_bind(location.radius_km * 1000.0); + } + + // Pagination and sorting + query.push(" ORDER BY "); + // ... sorting logic + query.push(" LIMIT ") + .push_bind(criteria.page_size) + .push(" OFFSET ") + .push_bind(criteria.page * criteria.page_size); + + Ok(query.build().fetch_all(&self.pool).await?) + } +} +``` + +## Cache Integration with Redis + +```rust +impl EventFilterService { + pub async fn filter_and_hydrate( + &self, + criteria: &EventFilterCriteria, + locale: &str + ) -> Result { + let cache_key = self.generate_filter_cache_key(criteria, locale); + + // Try cache first + if let Ok(Some(cached_data)) = self.cache_pool.get::(&cache_key).await { + tracing::debug!("Cache hit for filter results: {}", cache_key); + return Ok(cached_data); + } + + // Cache miss - perform database query and hydration + tracing::debug!("Cache miss for filter results: {}", cache_key); + + // Execute query, hydrate events, calculate facets + // ... + + // Store in cache with TTL + let _ = self.cache_pool + .set_with_expiry(&cache_key, &results, self.config.cache_ttl) + .await; + + Ok(results) + } + + fn generate_filter_cache_key(&self, criteria: &EventFilterCriteria, locale: &str) -> String { + // Create a stable hash from filter criteria + language + let mut hasher = DefaultHasher::new(); + criteria.hash(&mut hasher); + let criteria_hash = hasher.finish(); + + format!("filter:results:{}:{}", locale, criteria_hash) + } +} +``` + +## Facet Calculation Logic + +```rust +pub async fn calculate_facets( + pool: &PgPool, + criteria: &EventFilterCriteria, + locale: &str +) -> Result { + // Calculate categories without applying the category filter itself + let categories = sqlx::query!( + r#" + SELECT DISTINCT + jsonb_array_elements_text(record->'content'->'categories') as category, + COUNT(*) as count + FROM events + WHERE 1=1 + -- Apply all other criteria except categories + GROUP BY category + ORDER BY count DESC + LIMIT 20 + "# + ) + .fetch_all(pool) + .await?; + + // Transform into facets with i18n keys + let category_facets = categories.into_iter() + .map(|r| CategoryFacet { + name: r.category.unwrap_or_default(), + count: r.count as usize, + selected: criteria.categories.contains(&r.category.unwrap_or_default()), + i18n_key: format!("category-{}", r.category.unwrap_or_default() + .to_lowercase().replace(" ", "-")), + }) + .collect(); + + // Calculate other facets (date ranges, locations) + // ... + + Ok(EventFacets { + categories: category_facets, + dates: calculate_date_facets(pool, criteria).await?, + locations: calculate_location_facets(pool, criteria).await?, + }) +} +``` + +## HTMX Template Integration + +```html + +
+
+ + + +
+

{{ t(key='categories', locale=locale) }}

+ {% for category in facets.categories %} + + {% endfor %} +
+ + +
+
+ +
+ {% include "events/results.html" %} +
+``` + +## HTTP Handler Implementation + +```rust +pub async fn list_events( + ctx: UserRequestContext, + filter_criteria: Extension, +) -> impl IntoResponse { + let is_htmx = is_htmx_request(&ctx.request); + + // Filter & hydrate events + let filter_service = EventFilterService::new( + ctx.web_context.pool.clone(), + ctx.web_context.http_client.clone(), + ctx.web_context.cache_pool.clone() + ); + + let results = match filter_service.filter_and_hydrate( + &filter_criteria, + &ctx.language.0.to_string() + ).await { + Ok(r) => r, + Err(e) => { + tracing::error!(error = %e, "Failed to filter events"); + return (StatusCode::INTERNAL_SERVER_ERROR, + render_error_alert(&ctx, "error-filter-failed")).into_response(); + } + }; + + // Choose template based on request type + let template_name = if is_htmx { + format!("events/results.{}.html", ctx.language.0) + } else { + format!("events/index.{}.html", ctx.language.0) + }; + + // Render with i18n + render_with_i18n( + ctx.web_context.engine.clone(), + template_name, + ctx.language.0, + template_context! { + events => results.events, + facets => results.facets, + search_term => filter_criteria.search_term, + // Other context values... + } + ) +} +``` + +## Implementation Strategy + +The module should be implemented in phases: + +1. **Phase 1**: Core filter criteria and query building + - Define filter criteria types + - Implement SQL query builder + - Create basic middleware for extraction + +2. **Phase 2**: Facet calculation and hydration + - Implement facet calculation queries + - Build ATproto hydration service + - Set up basic templates + +3. **Phase 3**: Cache integration + - Integrate with Redis cache + - Set up cache invalidation + - Implement progressive caching + +4. **Phase 4**: I18n integration + - Add i18n keys to facets + - Integrate with HTMX language propagation + - Update templates to use i18n functions + +5. **Phase 5**: UI refinement and optimization + - Improve template responsiveness + - Add mobile-friendly filters + - Optimize performance + +## Testing Requirements + +Tests should cover: + +1. **Unit tests** for filter criteria extraction and query building + ```rust + #[test] + fn test_location_filter_query_building() { + // Test geographical filtering + } + ``` + +2. **Integration tests** for facet calculation + ```rust + #[sqlx::test] + async fn test_category_facets_calculation() { + // Test facet calculation with sample data + } + ``` + +3. **I18n tests** for facet translation + ```rust + #[test] + fn test_facet_i18n_keys_generated_correctly() { + // Test i18n key generation for facets + } + ``` + +4. **Cache tests** for proper invalidation + ```rust + #[test] + async fn test_cache_invalidation_on_event_update() { + // Test cache keys are properly invalidated + } + ``` + +5. **HTMX interaction** tests + ```rust + #[test] + async fn test_htmx_filter_updates() { + // Test HTMX responses contain correct headers + } + ``` + +## Performance Considerations + +- Use batch loading for ATproto hydration +- Apply tiered caching (facets vs. hydrated events) +- Implement conditional facet calculation +- Use optimized SQL queries with appropriate indexes +- Consider adding JSONB GIN indexes on event categories + +## Migration Plan + +When implementing this module: + +1. Create a feature flag `event-filtering` to enable/disable the feature +2. Add a migration for geospatial indexes if needed +3. Deploy the core filtering features first, without facets +4. Add facets and i18n integration in subsequent releases +5. Implement advanced caching as a final optimization + +gagnfra: add thoses guidelines to the technical summary + + + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + + + +## Common Commands + +- **Build**: `cargo build` +- **Run tests**: `cargo test` +- **Run specific test**: `cargo test test_name` +- **Check code**: `cargo check` +- **Format code**: `cargo fmt` +- **Lint**: `cargo clippy` +- **Run CLI tool**: `cargo run --bin atproto-identity-resolve -- ` +- **Run CLI with DID document**: `cargo run --bin atproto-identity-resolve -- --did-document ` + +## Architecture + +A comprehensive Rust library with: +- Modular architecture with 8 core modules (resolve, plc, web, model, validation, config, errors, key) +- Complete CLI tool for identity resolution (`atproto-identity-resolve`) +- Rust edition 2021 with modern async/await patterns +- Comprehensive error handling with structured error types +- Full test coverage with unit tests for all modules + +## Error Handling + +All error strings must use this format: + + error-atproto-identity-- :
+ +Example errors: + +* error-atproto-identity-resolve-1 Multiple DIDs resolved for method +* error-atproto-identity-plc-1 HTTP request failed: https://google.com/ Not Found +* error-atproto-identity-key-1 Error decoding key: invalid + +Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. + +Avoid creating new errors with the `anyhow!(...)` macro. + +When a function call would return `anyhow::Error`, use the following pattern to log the error in addition to any code specific handling that must occur + +``` +If let Err(err) = result { + tracing::error!(error = ?error, "Helpful contextual log line."); +} +``` + +## Result + +Functions that return a `Result` type should use `anyhow::Result` where second Error component of is one of the error types defined in `src/errors.rs`. + +## Logging + +Use tracing for structured logging. + +Async calls should be instrumented using the `.instrument()` that references the `use tracing::Instrument;` trait. + +## Documentation + +All public and exported types, methods, and variables must be documented. + +All source files must have high level module documentation. + +Documentation must be brief and specific. + + +GitHub Copilot: # Smokesignal Event Filtering Module - Technical Summary + +## Project Context + +This document summarizes the design and implementation approach for a new event filtering module in the Smokesignal application, a Rust-based social platform built on ATproto. The module provides faceted search and filtering capabilities for events while integrating with the existing i18n and caching infrastructure. + +## Core Requirements + +1. **Filtering Capabilities**: Support filtering events by multiple criteria including text search, dates, categories, and geolocation +2. **Faceted Navigation**: Display available filtering options with counts for each facet value +3. **HTMX Integration**: Support partial page updates with stateful filtering +4. **I18n Support**: Full internationalization of filters and facets +5. **ATproto Hydration**: Populate events with user profiles and related data +6. **Redis Cache Integration**: Optimize performance using existing cache infrastructure + +## Architecture Overview + +``` +src/filtering/ +├── mod.rs # Exports and FilterContext structure +├── criteria.rs # Filter criteria types +├── query_builder.rs # SQL query construction +├── facets.rs # Facet calculation logic +└── hydration.rs # ATproto entity hydration + +src/http/ +├── middleware_filter.rs # Filter extraction middleware +└── templates_filter.html # HTMX-compatible templates +``` + +## Event Filter Criteria Model + +```rust +#[derive(Debug, Clone, Default, Hash)] +pub struct EventFilterCriteria { + pub search_term: Option, + pub categories: Vec, + pub start_date: Option>, + pub end_date: Option>, + pub location: Option, + pub creator_did: Option, + pub page: usize, + pub page_size: usize, + pub sort_by: EventSortField, + pub sort_order: SortOrder, +} + +#[derive(Debug, Clone)] +pub struct LocationFilter { + pub latitude: f64, + pub longitude: f64, + pub radius_km: f64, +} +``` + +## I18n Integration Requirements + +The filtering module must integrate with the application's existing i18n system: + +1. **Template Functions**: Use direct template functions instead of pre-rendered translations + ```html +

{{ t(key="categories", locale=locale) }}

+ ``` + +2. **Facet Translation**: Support translation of facet values + ```rust + // Create i18n keys for facet values + category.i18n_key = format!("category-{}", category.name.to_lowercase() + .replace(" ", "-").replace("&", "and")); + ``` + +3. **HTMX Language Propagation**: Work with the language middleware + ```html +
+ +
+ ``` + +## QueryBuilder Pattern + +```rust +pub struct EventQueryBuilder { + pool: PgPool, +} + +impl EventQueryBuilder { + pub async fn build_and_execute( + &self, + criteria: &EventFilterCriteria + ) -> Result, FilterError> { + let mut query = sqlx::QueryBuilder::new("SELECT * FROM events WHERE 1=1 "); + + // Apply filters conditionally + if let Some(term) = &criteria.search_term { + query.push(" AND (name ILIKE "); + query.push_bind(format!("%{}%", term)); + query.push(")"); + } + + // Location filtering using PostGIS + if let Some(location) = &criteria.location { + query.push(" AND ST_DWithin( + ST_MakePoint((record->'location'->>'longitude')::float8, + (record->'location'->>'latitude')::float8)::geography, + ST_MakePoint($1, $2)::geography, + $3 + )"); + query.push_bind(location.longitude); + query.push_bind(location.latitude); + query.push_bind(location.radius_km * 1000.0); + } + + // Pagination and sorting + query.push(" ORDER BY "); + // ... sorting logic + query.push(" LIMIT ") + .push_bind(criteria.page_size) + .push(" OFFSET ") + .push_bind(criteria.page * criteria.page_size); + + Ok(query.build().fetch_all(&self.pool).await?) + } +} +``` + +## Cache Integration with Redis + +```rust +impl EventFilterService { + pub async fn filter_and_hydrate( + &self, + criteria: &EventFilterCriteria, + locale: &str + ) -> Result { + let cache_key = self.generate_filter_cache_key(criteria, locale); + + // Try cache first + if let Ok(Some(cached_data)) = self.cache_pool.get::(&cache_key).await { + tracing::debug!("Cache hit for filter results: {}", cache_key); + return Ok(cached_data); + } + + // Cache miss - perform database query and hydration + tracing::debug!("Cache miss for filter results: {}", cache_key); + + // Execute query, hydrate events, calculate facets + // ... + + // Store in cache with TTL + let _ = self.cache_pool + .set_with_expiry(&cache_key, &results, self.config.cache_ttl) + .await; + + Ok(results) + } + + fn generate_filter_cache_key(&self, criteria: &EventFilterCriteria, locale: &str) -> String { + // Create a stable hash from filter criteria + language + let mut hasher = DefaultHasher::new(); + criteria.hash(&mut hasher); + let criteria_hash = hasher.finish(); + + format!("filter:results:{}:{}", locale, criteria_hash) + } +} +``` + +## Facet Calculation Logic + +```rust +pub async fn calculate_facets( + pool: &PgPool, + criteria: &EventFilterCriteria, + locale: &str +) -> Result { + // Calculate categories without applying the category filter itself + let categories = sqlx::query!( + r#" + SELECT DISTINCT + jsonb_array_elements_text(record->'content'->'categories') as category, + COUNT(*) as count + FROM events + WHERE 1=1 + -- Apply all other criteria except categories + GROUP BY category + ORDER BY count DESC + LIMIT 20 + "# + ) + .fetch_all(pool) + .await?; + + // Transform into facets with i18n keys + let category_facets = categories.into_iter() + .map(|r| CategoryFacet { + name: r.category.unwrap_or_default(), + count: r.count as usize, + selected: criteria.categories.contains(&r.category.unwrap_or_default()), + i18n_key: format!("category-{}", r.category.unwrap_or_default() + .to_lowercase().replace(" ", "-")), + }) + .collect(); + + // Calculate other facets (date ranges, locations) + // ... + + Ok(EventFacets { + categories: category_facets, + dates: calculate_date_facets(pool, criteria).await?, + locations: calculate_location_facets(pool, criteria).await?, + }) +} +``` + +## HTMX Template Integration + +```html + +
+
+ + + +
+

{{ t(key='categories', locale=locale) }}

+ {% for category in facets.categories %} + + {% endfor %} +
+ + +
+
+ +
+ {% include "events/results.html" %} +
+``` + +## HTTP Handler Implementation + +```rust +pub async fn list_events( + ctx: UserRequestContext, + filter_criteria: Extension, +) -> impl IntoResponse { + let is_htmx = is_htmx_request(&ctx.request); + + // Filter & hydrate events + let filter_service = EventFilterService::new( + ctx.web_context.pool.clone(), + ctx.web_context.http_client.clone(), + ctx.web_context.cache_pool.clone() + ); + + let results = match filter_service.filter_and_hydrate( + &filter_criteria, + &ctx.language.0.to_string() + ).await { + Ok(r) => r, + Err(e) => { + tracing::error!(error = %e, "Failed to filter events"); + return (StatusCode::INTERNAL_SERVER_ERROR, + render_error_alert(&ctx, "error-filter-failed")).into_response(); + } + }; + + // Choose template based on request type + let template_name = if is_htmx { + format!("events/results.{}.html", ctx.language.0) + } else { + format!("events/index.{}.html", ctx.language.0) + }; + + // Render with i18n + render_with_i18n( + ctx.web_context.engine.clone(), + template_name, + ctx.language.0, + template_context! { + events => results.events, + facets => results.facets, + search_term => filter_criteria.search_term, + // Other context values... + } + ) +} +``` + +## Implementation Strategy + +The module should be implemented in phases: + +1. **Phase 1**: Core filter criteria and query building + - Define filter criteria types + - Implement SQL query builder + - Create basic middleware for extraction + +2. **Phase 2**: Facet calculation and hydration + - Implement facet calculation queries + - Build ATproto hydration service + - Set up basic templates + +3. **Phase 3**: Cache integration + - Integrate with Redis cache + - Set up cache invalidation + - Implement progressive caching + +4. **Phase 4**: I18n integration + - Add i18n keys to facets + - Integrate with HTMX language propagation + - Update templates to use i18n functions + +5. **Phase 5**: UI refinement and optimization + - Improve template responsiveness + - Add mobile-friendly filters + - Optimize performance + +## Testing Requirements + +Tests should cover: + +1. **Unit tests** for filter criteria extraction and query building + ```rust + #[test] + fn test_location_filter_query_building() { + // Test geographical filtering + } + ``` + +2. **Integration tests** for facet calculation + ```rust + #[sqlx::test] + async fn test_category_facets_calculation() { + // Test facet calculation with sample data + } + ``` + +3. **I18n tests** for facet translation + ```rust + #[test] + fn test_facet_i18n_keys_generated_correctly() { + // Test i18n key generation for facets + } + ``` + +4. **Cache tests** for proper invalidation + ```rust + #[test] + async fn test_cache_invalidation_on_event_update() { + // Test cache keys are properly invalidated + } + ``` + +5. **HTMX interaction** tests + ```rust + #[test] + async fn test_htmx_filter_updates() { + // Test HTMX responses contain correct headers + } + ``` + +## Performance Considerations + +- Use batch loading for ATproto hydration +- Apply tiered caching (facets vs. hydrated events) +- Implement conditional facet calculation +- Use optimized SQL queries with appropriate indexes +- Consider adding JSONB GIN indexes on event categories + +## Migration Plan + +When implementing this module: + +1. Create a feature flag `event-filtering` to enable/disable the feature +2. Add a migration for geospatial indexes if needed +3. Deploy the core filtering features first, without facets +4. Add facets and i18n integration in subsequent releases +5. Implement advanced caching as a final optimization + +## I18n Development Guidelines + +### I18n Architecture Goals + +- **HTMX-first design**: Seamless language propagation across partial page updates +- **Performance-optimized**: On-demand translation calculation instead of pre-rendering +- **Romance language support**: Gender agreement (masculine/feminine/neutral) +- **Fluent-based**: Mozilla Fluent for sophisticated translation features +- **Template integration**: Direct i18n functions in Jinja2 templates + +### Core Modules Structure + +``` +src/i18n/ +├── mod.rs # Main i18n exports and Locales struct +├── errors.rs # Structured error types for i18n operations +├── fluent_loader.rs # Fluent file loading (embed vs reload modes) +└── template_helpers.rs # Template function integration + +src/http/ +├── middleware_i18n.rs # HTMX-aware language detection middleware +├── template_i18n.rs # Template context with gender support +└── templates.rs # Template rendering with integrated i18n functions +``` + +### Language Detection Priority + +Implement language detection with this exact priority order for HTMX compatibility: + +1. **HX-Current-Language header** (highest priority for HTMX requests) +2. **User profile language** (if authenticated) +3. **lang cookie** (session preference) +4. **Accept-Language header** (browser preference) +5. **Default language** (fallback) + +### Template Integration Pattern + +Replace pre-rendered translation HashMap with direct template functions: + +#### ❌ Avoid (pre-rendering approach) +```rust +// Don't pre-calculate all translations +let mut translations = HashMap::new(); +translations.insert("profile-greeting".to_string(), i18n_context.tg(...)); +``` + +#### ✅ Use (on-demand functions) +```rust +// Register i18n functions in template engine +env.add_function("t", |args| { /* basic translation */ }); +env.add_function("tg", |args| { /* gender-aware translation */ }); +env.add_function("tc", |args| { /* count-based pluralization */ }); +``` + +### HTMX Integration Requirements + +#### Middleware Implementation +```rust +pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { + let is_htmx = request.headers().get("HX-Request").is_some(); + + // Detect language with HTMX priority + let locale = detect_language_with_htmx_priority(&request); + + // Inject into request extensions + request.extensions_mut().insert(Language(locale.clone())); + + let mut response = next.run(request).await; + + // Add language propagation header for HTMX + if is_htmx { + response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); + } + + response +} +``` + +### Gender Support + +```rust +#[derive(Debug, Clone)] +pub enum Gender { + Masculine, + Feminine, + Neutral, +} + +impl Gender { + pub fn as_str(&self) -> &'static str { + match self { + Gender::Masculine => "masculine", + Gender::Feminine => "feminine", + Gender::Neutral => "neutral", + } + } +} +``` + +### Fluent File Organization + +``` +i18n/ +├── en-us/ +│ ├── common.ftl # Shared UI elements +│ ├── errors.ftl # Error messages +│ └── ui.ftl # Interface text +└── fr-ca/ + ├── common.ftl + ├── errors.ftl + └── ui.ftl +``` + +### Error Handling + +All i18n error strings must follow this format: +``` +error-smokesignal-i18n-- :
+``` + +Example errors: +``` +error-smokesignal-i18n-fluent-1 Translation key not found: profile-greeting +error-smokesignal-i18n-locale-2 Unsupported language identifier: xx-XX +error-smokesignal-i18n-template-3 Template function argument missing: locale +``` + +### Code Comments + +Keep all code comments in English: +```rust +// Create i18n context with user-specific gender preferences +let i18n_context = TemplateI18nContext::new(locale, locales) + .with_gender(user_gender.unwrap_or(Gender::Neutral)); +``` + +### Ressources + +https://docs.rs/axum-template/3.0.0/axum_template/index.html +https://docs.rs/minijinja/latest/minijinja/index.html +https://github.com/projectfluent/fluent/wiki/ diff --git a/docs/i18n.md b/docs/i18n.md deleted file mode 100644 index 6099e29..0000000 --- a/docs/i18n.md +++ /dev/null @@ -1,2312 +0,0 @@ -gagnfra: Explique moi ce fichier en détails. soit court et concis pour un débutant - -GitHub Copilot: Ce fichier smokesignal.rs est le **point d'entrée principal** d'une application web Rust. Voici ce qu'il fait : - -## Vue d'ensemble -C'est un serveur web qui gère l'internationalisation (i18n) et utilise plusieurs technologies modernes. - -## Étapes principales - -### 1. **Configuration initiale** -- Configure les logs avec `tracing` -- Charge la configuration de l'app -- Crée un client HTTP avec des certificats personnalisés - -### 2. **Base de données** -- Se connecte à PostgreSQL (`PgPool`) -- Exécute les migrations de base de données -- Se connecte à Redis pour le cache - -### 3. **Internationalisation** -- Supporte l'anglais américain (`en-us`) et le français canadien (`fr-ca`) -- Charge les traductions selon le mode : - - `embed` : traductions intégrées dans l'exécutable - - `reload` : traductions rechargées dynamiquement - -### 4. **Moteur de templates** -- Configure Jinja2 pour générer les pages web -- Intègre le système de traduction - -### 5. **Serveur web** -- Crée le contexte web avec toutes les dépendances -- Lance le serveur sur le port configuré -- Gère l'arrêt gracieux (Ctrl+C) - -### 6. **Tâches en arrière-plan** -- Lance une tâche pour rafraîchir les tokens OAuth -- Utilise un système de tracking des tâches - -## Technologies utilisées -- **Axum** : framework web -- **SQLx** : accès base de données -- **Redis** : cache -- **Tokio** : runtime asynchrone -- **Jinja2** : moteur de templates - -C'est une architecture moderne et robuste pour une application web multilingue. - -gagnfra: explique moi chaqu'un des modules du stack - -GitHub Copilot: Voici l'explication de chaque module utilisé dans ce stack : - -## **Modules Rust Standards** -- **`anyhow`** : Gestion d'erreurs simplifiée avec `Result<()>` -- **`std::sync::Arc`** : Partage de données entre threads de façon sécurisée -- **`std::str::FromStr`** : Conversion de chaînes en types - -## **Modules Temps & Async** -- **`chrono`** : Manipulation de dates/heures (`Duration`) -- **`tokio`** : Runtime asynchrone pour gérer les tâches concurrentes -- **`tokio::net::TcpListener`** : Serveur TCP asynchrone -- **`tokio::signal`** : Gestion des signaux système (Ctrl+C) -- **`tokio_util::task::TaskTracker`** : Suivi des tâches en arrière-plan - -## **Modules Web & HTTP** -- **`axum`** : Framework web moderne pour Rust -- **`reqwest`** : Client HTTP pour faire des requêtes externes - -## **Modules Base de Données** -- **`sqlx::PgPool`** : Connexion PostgreSQL avec pool de connexions -- **`sqlx::migrate!()`** : Migrations automatiques de DB - -## **Modules Cache & Storage** -- **`smokesignal::storage::cache`** : Abstraction pour Redis -- **Redis** : Cache en mémoire pour les performances - -## **Modules Internationalisation** -- **`unic_langid::LanguageIdentifier`** : Identifiants de langues (en-us, fr-ca) -- **`smokesignal::i18n::Locales`** : Système de traductions - -## **Modules Templates** -- **Jinja2** (via `smokesignal::http::templates`) : Moteur de templates pour HTML -- **Features `embed`/`reload`** : Compilation statique vs rechargement dynamique - -## **Modules Réseau** -- **`smokesignal::resolve`** : Résolveur DNS personnalisé -- **Certificats SSL** : Gestion des CA certificates - -## **Modules Monitoring** -- **`tracing`** : Logs structurés et observabilité -- **`tracing_subscriber`** : Configuration des logs - -## **Modules Métier** -- **`RefreshTokensTask`** : Tâche de rafraîchissement OAuth -- **`WebContext`** : Contexte applicatif partagé -- **`build_router`** : Configuration des routes web - -Chaque module a un rôle spécifique dans cette architecture moderne et scalable. - -gagnfra: Meme chose avec celui ci - -GitHub Copilot: Voici l'explication détaillée du module d'internationalisation (i18n.rs) : - -## **Vue d'ensemble** -Ce module gère le **système de traductions multilingues** en utilisant Mozilla Fluent, un système moderne d'internationalisation. - -## **Dépendances principales** - -### **Fluent Framework** -- **`fluent::FluentBundle`** : Conteneur pour les traductions d'une langue -- **`fluent::FluentResource`** : Fichier de traduction (.ftl) parsé -- **`fluent::FluentArgs`** : Arguments pour les messages avec variables -- **`intl_memoizer`** : Cache pour optimiser les performances - -### **Autres modules** -- **`unic_langid::LanguageIdentifier`** : Identifiants de langues (en-us, fr-ca) -- **`rust_embed`** : Intégration de fichiers dans l'exécutable -- **`thiserror`** : Gestion d'erreurs typées - -## **Structure principale** - -### **`Bundle`** (Type alias) -```rust -type Bundle = FluentBundle -``` -Un bundle contient toutes les traductions pour une langue donnée. - -### **`Locales`** (Struct principale) -```rust -struct Locales(HashMap) -``` -- Stocke un bundle par langue supportée -- Implémente `Clone` manuellement (Bundle ne l'implémente pas) - -## **Méthodes principales** - -### **`new()`** - Construction -Crée un bundle vide pour chaque langue supportée. - -### **`add_bundle()`** - Chargement -- Parse un fichier `.ftl` (format Fluent) -- Ajoute les traductions au bundle de la langue - -### **`format_message()`** - Traduction -- Récupère un message traduit par sa clé -- Substitue les variables avec `FluentArgs` -- Retourne la clé si traduction introuvable - -### **`format_error()`** - Erreurs -Version simplifiée pour les messages d'erreur. - -## **Modes de chargement** - -### **Mode `embed`** (Production) -```rust -#[derive(Embed)] -#[folder = "i18n/"] -struct I18nAssets; -``` -- Fichiers `.ftl` intégrés dans l'exécutable -- Plus rapide, pas de I/O disque -- Nécessite recompilation pour changer les traductions - -### **Mode `reload`** (Développement) -```rust -let source_file = locale_dir.join(format!("{}.ftl", file)); -let content = std::fs::read(source_file)?; -``` -- Lit les fichiers depuis le disque -- Permet modification à chaud -- Plus lent mais plus flexible - -## **Types de fichiers** -Charge 3 types de traductions pour chaque langue : -- **`errors.ftl`** : Messages d'erreur backend -- **`ui.ftl`** : Interface utilisateur -- **`common.ftl`** : Messages communs - -## **Gestion d'erreurs** -```rust -enum I18nError { - InvalidLanguage, // Langue non supportée - LanguageResourceFailed, // Erreur parsing .ftl - BundleLoadFailed, // Erreur chargement bundle -} -``` - -## **Architecture robuste** -- **Fallback** : Retourne la clé si traduction manquante -- **Thread-safe** : Utilise `concurrent::IntlLangMemoizer` -- **Performance** : Cache intégré pour éviter re-parsing - -Ce système permet une internationalisation complète et performante de l'application. - -gagnfra: explique moi celui ci - -GitHub Copilot: Voici l'explication détaillée du module context.rs : - -## **Vue d'ensemble** -Ce module définit les **contextes partagés** entre les différentes parties de l'application web. Il centralise toutes les dépendances nécessaires aux handlers HTTP. - -## **Structures principales** - -### **`I18nContext`** - Contexte d'internationalisation -```rust -struct I18nContext { - supported_languages: Vec, // Langues supportées - locales: Arc, // Traductions partagées -} -``` -Contient tout ce qui concerne les traductions. - -### **`InnerWebContext`** - Contexte applicatif complet -```rust -struct InnerWebContext { - engine: AppEngine, // Moteur de templates (Jinja2) - http_client: reqwest::Client, // Client HTTP externe - pool: StoragePool, // Pool connexions PostgreSQL - cache_pool: CachePool, // Pool connexions Redis - config: Config, // Configuration app - i18n_context: I18nContext, // Contexte traductions - dns_resolver: TokioAsyncResolver, // Résolveur DNS -} -``` -Regroupe **toutes les dépendances** de l'application. - -### **`WebContext`** - Wrapper thread-safe -```rust -#[derive(Clone, FromRef)] -struct WebContext(Arc); -``` -- Utilise `Arc` pour partage entre threads -- `Clone` bon marché (juste un pointeur) -- `FromRef` permet extraction automatique par Axum - -## **Pattern de conception** - -### **Deref Trait** -```rust -impl Deref for WebContext { - type Target = InnerWebContext; - fn deref(&self) -> &Self::Target { &self.0 } -} -``` -Permet d'accéder directement aux champs : `context.pool` au lieu de `context.0.pool`. - -## **Contextes spécialisés** - -### **`AdminRequestContext`** - Handlers admin -```rust -struct AdminRequestContext { - web_context: WebContext, - language: Language, // Langue de l'utilisateur - admin_handle: Handle, // Utilisateur admin validé - auth: Auth, // Infos d'authentification -} -``` - -**Extraction automatique** : -- Récupère le contexte web -- Extrait la langue de la requête -- **Valide que l'utilisateur est admin** -- Retourne erreur si pas admin - -### **`UserRequestContext`** - Handlers utilisateur -```rust -struct UserRequestContext { - web_context: WebContext, - language: Language, - current_handle: Option, // Utilisateur connecté (optionnel) - auth: Auth, -} -``` - -Plus permissif, utilisateur peut être non-connecté. - -## **Features conditionnelles** - -### **Mode `embed` vs `reload`** -```rust -#[cfg(feature = "embed")] -type AppEngine = Engine>; - -#[cfg(feature = "reload")] -type AppEngine = Engine; -``` -- **Embed** : Templates compilés dans l'exécutable -- **Reload** : Templates rechargés automatiquement - -## **Intégration Axum** - -### **`FromRequestParts`** -Les contextes implémentent ce trait pour être **extraits automatiquement** dans les handlers : - -```rust -async fn admin_handler(ctx: AdminRequestContext) -> Response { - // ctx contient automatiquement tout ce dont on a besoin -} -``` - -### **Validation automatique** -- `AdminRequestContext` vérifie automatiquement les droits admin -- Retourne erreur HTTP si validation échoue -- Évite la duplication de code de validation - -## **Helpers utilitaires** - -### **`admin_template_context()`** -```rust -fn admin_template_context(ctx: &AdminRequestContext, canonical_url: &str) -> Value -``` -Crée un contexte standard pour les templates admin avec : -- Langue courante -- Handle utilisateur -- URL canonique - -## **Avantages de cette architecture** - -1. **Centralisation** : Toutes les dépendances en un endroit -2. **Type safety** : Validation des permissions au niveau type -3. **Réutilisabilité** : Contextes spécialisés par cas d'usage -4. **Performance** : `Arc` évite les copies coûteuses -5. **Simplicité** : Extraction automatique par Axum - -Cette approche rend le code des handlers **très propre et sûr**. - -gagnfra: meme chose pour celui ci - -GitHub Copilot: Voici l'explication détaillée du module templates.rs : - -## **Vue d'ensemble** -Ce module gère le **rendu des templates HTML** avec support complet de l'internationalisation (i18n). Il utilise Jinja2 comme moteur de templates. - -## **Dépendances principales** - -### **Templates & Rendu** -- **`axum_template::RenderHtml`** : Wrapper pour rendu HTML avec Axum -- **`minijinja`** : Port Rust de Jinja2 (moteur de templates Python) -- **`minijinja_autoreload`** : Rechargement automatique en développement -- **`minijinja_embed`** : Intégration de templates dans l'exécutable - -### **Internationalisation** -- **`TemplateI18nContext`** : Contexte i18n avec genre/formalité -- **`Gender`** : Masculin/Féminin/Neutre pour accords -- **`Formality`** : Formel/Informel pour vouvoiement - -## **Fonctions principales** - -### **`render_alert()`** - Alertes simples -```rust -fn render_alert(engine, language, message, context) -> Response -``` -- Rendu rapide pour messages d'alerte -- Template `prompt.{language}.html` -- Contexte minimal avec juste le message - -### **`render_with_i18n()`** - Rendu complet avec i18n -**Fonction centrale** qui : - -1. **Crée le contexte i18n** avec genre/formalité utilisateur -2. **Pré-calcule les traductions** courantes -3. **Extrait paramètres** du contexte (ex: handle du profil) -4. **Génère le contexte template** final - -#### **Traductions pré-calculées** -```rust -translations.insert("profile-greeting", i18n_context.tgf("profile-greeting", ...)); -translations.insert("follow-user", i18n_context.tf("follow-user", ...)); -translations.insert("events-created", i18n_context.tc("events-created", 5, ...)); -``` - -- **`t()`** : Traduction simple -- **`tf()`** : Traduction avec formalité -- **`tgf()`** : Traduction avec genre + formalité -- **`tc()`** : Traduction avec comptage (pluriels) - -#### **Contexte template enrichi** -```rust -template_context! { - locale => "en-us", - user_gender => "neutral", - user_formality => "informal", - language => "en", - region => "us", - translations => {...}, - ..additional_context -} -``` - -## **Modes de fonctionnement** - -### **Mode `reload`** (Développement) -```rust -AutoReloader::new(|notifier| { - let template_path = PathBuf::from("templates"); - env.set_loader(path_loader(&template_path)); - notifier.watch_path(&template_path, true); -}) -``` - -**Avantages** : -- Rechargement automatique des templates -- Modification à chaud sans restart -- Surveillance du système de fichiers - -### **Mode `embed`** (Production) -```rust -minijinja_embed::load_templates!(&mut env); -``` - -**Avantages** : -- Templates compilés dans l'exécutable -- Pas d'I/O disque en runtime -- Déploiement simplifié - -## **Configuration du moteur** - -### **Options Jinja2** -```rust -env.set_trim_blocks(true); // Supprime espaces automatiquement -env.set_lstrip_blocks(true); // Supprime indentation automatiquement -``` - -### **Variables globales** -```rust -env.add_global("base", "https://example.com"); // URL de base -env.add_global("version", "1.0.0"); // Version app -``` - -## **Architecture i18n avancée** - -### **Support genre/formalité** -```rust -let i18n_context = TemplateI18nContext::new(locale, locales) - .with_gender(Gender::Neutral) - .with_formality(Formality::Informal); -``` - -Permet des traductions comme : -- **Français** : "Bienvenue" vs "Bienvenu·e" (genre) -- **Français** : "tu" vs "vous" (formalité) - -### **Paramètres dynamiques** -```rust -let mut profile_subtitle_params = HashMap::new(); -profile_subtitle_params.insert("handle", "john_doe"); -i18n_context.tgf("profile-subtitle", None, None, Some(params)); -``` - -Traductions avec variables : `"Profil de {handle}"` - -## **Debug & Logging** -```rust -println!("DEBUG: Creating translations for locale: {}", locale); -println!("DEBUG: Template context created with {} translations", ...); -``` - -Aide au débogage des problèmes de traduction. - -## **Avantages de cette approche** - -1. **Performance** : Traductions pré-calculées -2. **Flexibilité** : Support genre/formalité -3. **Maintenabilité** : Séparation claire logique/présentation -4. **Robustesse** : Fallbacks si traductions manquantes -5. **Développement** : Hot-reload en mode dev - -Cette architecture permet une **internationalisation sophistiquée** tout en gardant les templates simples. - -gagnfra: quest ce qi pourrait être amélioré dans ce fichier ? - -GitHub Copilot: Voici les améliorations possibles pour ce fichier templates.rs : - -## **1. Performance & Mémoire** - -### **Traductions pré-calculées inefficaces** -```rust -// Problème : toutes les traductions sont calculées pour chaque rendu -let mut translations = std::collections::HashMap::new(); -translations.insert("profile-greeting".to_string(), i18n_context.tgf(...)); -// ... 20+ traductions -``` - -**Amélioration** : Cache des traductions -```rust -// Calculer une seule fois et réutiliser -static TRANSLATION_CACHE: Lazy>> = Lazy::new(DashMap::new); - -fn get_cached_translations(locale: &LanguageIdentifier, context: &TemplateI18nContext) -> HashMap { - let cache_key = format!("{}:{}:{}", locale, context.gender, context.formality); - TRANSLATION_CACHE.entry(cache_key).or_insert_with(|| compute_translations(context)).clone() -} -``` - -## **2. Code dupliqué** - -### **Logique répétitive** -```rust -translations.insert("profile".to_string(), i18n_context.t("profile", None)); -translations.insert("handle".to_string(), i18n_context.t("handle", None)); -// ... répété 20+ fois -``` - -**Amélioration** : Macro ou fonction helper -```rust -macro_rules! add_translations { - ($map:expr, $ctx:expr, [$(($key:expr, $method:ident $(, $args:expr)*)),*]) => { - $( - $map.insert($key.to_string(), $ctx.$method($key $(, $args)*)); - )* - }; -} - -// Usage -add_translations!(translations, i18n_context, [ - ("profile", t, None), - ("handle", t, None), - ("follow-user", tf, None, None), - ("events-created", tc, 5, None) -]); -``` - -## **3. Gestion d'erreurs** - -### **Extraction fragile du contexte** -```rust -// Peut paniquer si la structure change -if let Ok(profile_value) = additional_context.get_attr("profile") { - if let Ok(handle_value) = profile_value.get_attr("handle") { - if let Some(handle) = handle_value.as_str() { - profile_subtitle_params.insert("handle".to_string(), handle.to_string()); - } - } -} -``` - -**Amélioration** : Helper sûr -```rust -fn extract_nested_string(context: &minijinja::Value, path: &[&str]) -> Option { - let mut current = context; - for &key in path { - current = current.get_attr(key).ok()?; - } - current.as_str().map(|s| s.to_string()) -} - -// Usage -if let Some(handle) = extract_nested_string(&additional_context, &["profile", "handle"]) { - profile_subtitle_params.insert("handle".to_string(), handle); -} -``` - -## **4. Configuration centralisée** - -### **Traductions hardcodées** -```rust -// Liste hardcodée dans le code -translations.insert("profile-greeting".to_string(), ...); -translations.insert("first-visit-title".to_string(), ...); -``` - -**Amélioration** : Configuration externe -```rust -// translations_config.toml -[common_translations] -simple = ["profile", "handle", "actions"] -with_formality = ["follow-user", "profile-greeting"] -with_count = [ - { key = "events-created", default_count = 5 }, - { key = "rsvps-made", default_count = 12 } -] -``` - -## **5. Debug & Logging** - -### **println! en production** -```rust -println!("DEBUG: Creating translations for locale: {}", locale); -``` - -**Amélioration** : Logging conditionnel -```rust -tracing::debug!("Creating translations for locale: {}", locale); -#[cfg(debug_assertions)] -tracing::trace!("Template context: {:?}", template_context); -``` - -## **6. Architecture modulaire** - -### **Fonction trop longue** -La fonction `render_with_i18n` fait trop de choses (90+ lignes). - -**Amélioration** : Découpage -```rust -struct TemplateRenderer { - translation_cache: Arc>>, -} - -impl TemplateRenderer { - fn prepare_translations(&self, context: &TemplateI18nContext) -> HashMap { ... } - fn extract_parameters(&self, context: &minijinja::Value) -> HashMap { ... } - fn build_template_context(&self, ...) -> minijinja::Value { ... } - - pub fn render_with_i18n(&self, ...) -> impl IntoResponse { - let translations = self.prepare_translations(&i18n_context); - let params = self.extract_parameters(&additional_context); - let context = self.build_template_context(...); - RenderHtml(template_name, engine, context) - } -} -``` - -## **7. Type safety** - -### **Clés de traduction en String** -```rust -translations.insert("profile-greeting".to_string(), ...); -``` - -**Amélioration** : Enum typé -```rust -#[derive(Debug, Clone, Copy)] -enum TranslationKey { - ProfileGreeting, - FirstVisitTitle, - Actions, -} - -impl TranslationKey { - fn as_str(&self) -> &'static str { - match self { - Self::ProfileGreeting => "profile-greeting", - Self::FirstVisitTitle => "first-visit-title", - Self::Actions => "actions", - } - } -} -``` - -Ces améliorations rendraient le code plus **performant**, **maintenable** et **robuste**. - -gagnfra: les points 1 et 2 viennent du fair que le data doit être prérendu pour minijinja. serait il possible de se fier au fichier FTL ? - -GitHub Copilot: Excellente question ! Oui, il serait **beaucoup plus efficace** de s'appuyer directement sur les fichiers FTL plutôt que de pré-rendre toutes les traductions. - -## **Problème actuel** -```rust -// Pré-calcul inefficace de toutes les traductions -let mut translations = HashMap::new(); -translations.insert("profile-greeting".to_string(), i18n_context.tgf("profile-greeting", ...)); -translations.insert("first-visit-title".to_string(), i18n_context.tgf("first-visit-title", ...)); -// ... 20+ traductions calculées même si pas utilisées -``` - -## **Solution optimale : Fonctions template intégrées** - -### **1. Enregistrer les fonctions i18n dans MiniJinja** - -```rust -pub fn build_env_with_i18n( - http_external: String, - version: String, - default_locale: LanguageIdentifier, - locales: Arc -) -> Environment<'static> { - let mut env = build_env(http_external, version); - - // Créer les fonctions de traduction - let locales_clone = locales.clone(); - let default_locale_clone = default_locale.clone(); - - // Fonction de traduction de base - env.add_function("t", move |key: String, args: Option| -> String { - let context = TemplateI18nContext::new(default_locale_clone.clone(), locales_clone.clone()); - let args_map = value_to_hashmap(args); - context.t(&key, args_map) - }); - - // Fonction avec comptage - env.add_function("tc", move |key: String, count: i32, args: Option| -> String { - let context = TemplateI18nContext::new(default_locale_clone.clone(), locales_clone.clone()); - let args_map = value_to_hashmap(args); - context.tc(&key, count, args_map) - }); - - // Fonction avec genre - env.add_function("tg", move |key: String, gender: Option, args: Option| -> String { - let context = TemplateI18nContext::new(default_locale_clone.clone(), locales_clone.clone()); - let gender_enum = gender.map(|g| Gender::from_str(&g)); - let args_map = value_to_hashmap(args); - context.tg(&key, gender_enum, args_map) - }); - - // Fonction avec formalité - env.add_function("tf", move |key: String, formality: Option, args: Option| -> String { - let context = TemplateI18nContext::new(default_locale_clone.clone(), locales_clone.clone()); - let formality_enum = formality.map(|f| Formality::from_str(&f)); - let args_map = value_to_hashmap(args); - context.tf(&key, formality_enum, args_map) - }); - - // Fonction genre + formalité - env.add_function("tgf", move |key: String, gender: Option, formality: Option, args: Option| -> String { - let context = TemplateI18nContext::new(default_locale_clone.clone(), locales_clone.clone()); - let gender_enum = gender.map(|g| Gender::from_str(&g)); - let formality_enum = formality.map(|f| Formality::from_str(&f)); - let args_map = value_to_hashmap(args); - context.tgf(&key, gender_enum, formality_enum, args_map) - }); - - env -} - -fn value_to_hashmap(value: Option) -> Option> { - value?.as_object()?.iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect::>() - .into() -} -``` - -### **2. Usage direct dans les templates** - -```html - -{{ tgf("profile-greeting", user_gender, user_formality) }} - - -{{ t("welcome-user", {"name": current_handle.display_name}) }} - - -{{ tc("events-created", profile.event_count) }} - - -{% if user_gender == "feminine" %} - {{ tg("welcome-message", "feminine") }} -{% else %} - {{ tg("welcome-message", "masculine") }} -{% endif %} -``` - -### **3. Contexte template simplifié** - -```rust -pub fn render_with_i18n( - engine: E, - template_name: String, - locale: LanguageIdentifier, - locales: Arc, - user_gender: Option, - user_formality: Option, - additional_context: minijinja::Value, -) -> impl IntoResponse { - // Plus besoin de pré-calculer les traductions ! - let template_context = template_context! { - locale => locale.to_string(), - user_gender => user_gender.as_ref().map(|g| g.as_str()).unwrap_or("neutral"), - user_formality => user_formality.as_ref().map(|f| f.as_str()).unwrap_or("informal"), - language => locale.language.as_str(), - region => locale.region.as_ref().map(|r| r.as_str()).unwrap_or(""), - ..additional_context - }; - - RenderHtml(template_name, engine, template_context) -} -``` - -## **Avantages de cette approche** - -### **Performance** -- ✅ **Calcul à la demande** : seules les traductions utilisées sont calculées -- ✅ **Cache Fluent** : Les bundles Fluent ont leur propre cache interne -- ✅ **Pas de pré-calcul** : Élimination du HashMap de traductions - -### **Maintenabilité** -- ✅ **DRY** : Plus de duplication de clés -- ✅ **Fichiers FTL source de vérité** : Seuls les .ftl définissent les traductions -- ✅ **Type safety** : Erreurs de clés détectées au runtime dans les templates - -### **Flexibilité** -- ✅ **Traductions dynamiques** : Paramètres calculés dans le template -- ✅ **Logique conditionnelle** : Genre/formalité déterminés dans le template -- ✅ **Réutilisabilité** : Templates plus génériques - -### **Exemple de fichier FTL** -```ftl -# common.ftl -profile-greeting = Hello -profile-greeting-feminine = Hello miss -profile-greeting-masculine = Hello sir -profile-greeting-feminine-formal = Good day madam -profile-greeting-masculine-formal = Good day sir - -welcome-user = Welcome {$name}! - -events-created = { $count -> - [0] No events created - [1] One event created - *[other] {$count} events created -} -``` - -Cette approche est **beaucoup plus élégante** et performante que le pré-rendu actuel ! - -gagnfra: est-ce qu'il fonctionneras dans le cas de relaod htmx boostés - -GitHub Copilot: **Excellente question !** Il y a effectivement des défis spécifiques avec **HTMX + reload** pour le système i18n. - -## **Problèmes potentiels** - -### **1. État de la langue perdu entre requêtes HTMX** -```html - -
- - -
-``` - -### **2. Contexte i18n non propagé** -```rust -// Dans un handler HTMX -async fn profile_partial(ctx: UserRequestContext) -> Response { - // ❌ Problème: quel locale utiliser pour ce fragment ? - render_with_i18n(engine, "profile_partial.html", ???, ...) -} -``` - -## **Solutions recommandées** - -### **1. Middleware de détection automatique** -```rust -// middleware_i18n.rs -pub async fn language_middleware( - request: Request, - next: Next, -) -> Response { - // Priorité de détection - let locale = detect_language(&request); - - // Injecter dans les headers de réponse HTMX - let mut response = next.run(request).await; - - if is_htmx_request(&request) { - response.headers_mut().insert( - "HX-Language", - HeaderValue::from_str(&locale.to_string()).unwrap() - ); - } - - response -} - -fn detect_language(request: &Request) -> LanguageIdentifier { - // 1. Header HTMX personnalisé (reqêtes suivantes) - if let Some(lang) = request.headers().get("HX-Current-Language") { - if let Ok(lang_str) = lang.to_str() { - if let Ok(locale) = LanguageIdentifier::from_str(lang_str) { - return locale; - } - } - } - - // 2. Cookie de session - if let Some(cookie_lang) = extract_language_from_cookies(request) { - return cookie_lang; - } - - // 3. Accept-Language header - if let Some(accept_lang) = extract_from_accept_language(request) { - return accept_lang; - } - - // 4. Fallback - "en-us".parse().unwrap() -} -``` - -### **2. HTMX avec propagation de langue** -```html - -
- -
- - - -``` - -### **3. Context enrichi pour HTMX** -```rust -// context.rs -impl FromRequestParts for UserRequestContext -where - S: Send + Sync, - WebContext: FromRef, -{ - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let web_context = WebContext::from_ref(state); - - // Détection spéciale pour HTMX - let language = if is_htmx_request(parts) { - // Utiliser header personnalisé ou cookie - Language::from_htmx_request(parts, state).await - } else { - // Détection standard - Language::from_request_parts(parts, state).await - }?; - - Ok(Self { - web_context, - language, - // ... - }) - } -} - -impl Language { - async fn from_htmx_request(parts: &Parts, _state: &S) -> Result { - // 1. Header HX-Current-Language - if let Some(lang_header) = parts.headers.get("HX-Current-Language") { - if let Ok(lang_str) = lang_header.to_str() { - if let Ok(locale) = LanguageIdentifier::from_str(lang_str) { - return Ok(Language(locale)); - } - } - } - - // 2. Cookie fallback - if let Some(cookies) = parts.headers.get(header::COOKIE) { - // Parser le cookie de langue - } - - // 3. Défaut - Ok(Language("en-us".parse().unwrap())) - } -} -``` - -### **4. Templates adaptés pour HTMX** -```rust -// templates.rs - Version HTMX-aware -pub fn render_htmx_with_i18n( - engine: E, - template_name: String, - locale: LanguageIdentifier, - locales: Arc, - is_htmx: bool, - additional_context: minijinja::Value, -) -> impl IntoResponse { - let template_context = template_context! { - locale => locale.to_string(), - is_htmx => is_htmx, - ..additional_context - }; - - let mut response = RenderHtml(template_name, engine, template_context).into_response(); - - // Ajouter headers HTMX pour maintenir l'état - if is_htmx { - response.headers_mut().insert( - "HX-Language", - HeaderValue::from_str(&locale.to_string()).unwrap() - ); - } - - response -} -``` - -### **5. Handlers HTMX avec langue maintenue** -```rust -// handlers/profile.rs -pub async fn profile_partial( - ctx: UserRequestContext, - HxRequest: HxRequest, // Extracteur HTMX -) -> impl IntoResponse { - let profile = get_user_profile(&ctx).await?; - - render_htmx_with_i18n( - ctx.web_context.engine.clone(), - "partials/profile.html".to_string(), - ctx.language.0, // Langue détectée automatiquement - ctx.web_context.i18n_context.locales.clone(), - true, // is_htmx - template_context! { - profile => profile, - } - ) -} -``` - -## **Templates avec fonctions i18n intégrées** - -```html - -
-

{{ t("profile-title") }}

-

{{ tgf("welcome-message", user_gender, user_formality) }}

- - - -
-``` - -## **Résultat** - -✅ **Fonctionne parfaitement** avec HTMX + reload car : - -1. **Détection automatique** de la langue via middleware -2. **Propagation** via headers HTTP personnalisés -3. **Fonctions i18n** disponibles dans tous les templates -4. **État maintenu** entre requêtes partielles -5. **Fallbacks robustes** si détection échoue - -Cette approche est **beaucoup plus robuste** que le pré-rendu actuel pour HTMX ! - -gagnfra: tous les formulaires présentement utilisent htmx pour mettre a jour la page. avec ta connaissance de cette discussion, quel serait alors la meilleure approche ? - -GitHub Copilot: Avec **tous les formulaires utilisant HTMX**, voici la **meilleure approche** basée sur notre discussion : - -## **Architecture recommandée : Fonctions i18n intégrées + Context HTMX-aware** - -### **1. Fonctions template globales (au lieu du pré-rendu)** - -```rust -// templates.rs - Version optimisée pour HTMX -pub fn build_env_with_i18n( - http_external: String, - version: String, - default_locale: LanguageIdentifier, - locales: Arc -) -> Environment<'static> { - let mut env = build_env(http_external, version); - - // Fonctions i18n globales avec closure sur locale par défaut - let locales_clone = locales.clone(); - let default_locale_clone = default_locale.clone(); - - env.add_function("t", move |args: minijinja::Value| -> Result { - let key = args.get_attr("key")?.to_string(); - let locale_str = args.get_attr("locale").ok() - .and_then(|v| v.as_str()) - .unwrap_or(&default_locale_clone.to_string()); - let locale = LanguageIdentifier::from_str(locale_str).unwrap_or(default_locale_clone.clone()); - - let context = TemplateI18nContext::new(locale, locales_clone.clone()); - Ok(context.t(&key, extract_args(&args))) - }); - - env.add_function("tgf", move |args: minijinja::Value| -> Result { - let key = args.get_attr("key")?.to_string(); - let locale_str = args.get_attr("locale").ok() - .and_then(|v| v.as_str()) - .unwrap_or(&default_locale_clone.to_string()); - let locale = LanguageIdentifier::from_str(locale_str).unwrap_or(default_locale_clone.clone()); - - let gender = args.get_attr("gender").ok() - .and_then(|v| v.as_str()) - .map(Gender::from_str); - let formality = args.get_attr("formality").ok() - .and_then(|v| v.as_str()) - .map(Formality::from_str); - - let context = TemplateI18nContext::new(locale, locales_clone.clone()); - Ok(context.tgf(&key, gender, formality, extract_args(&args))) - }); - - // ... autres fonctions tc, tg, tf - - env -} - -fn extract_args(value: &minijinja::Value) -> Option> { - value.get_attr("args").ok()? - .as_object()? - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect::>() - .into() -} -``` - -### **2. Contexte HTMX-aware simplifié** - -```rust -// templates.rs - Rendu optimisé -pub fn render_with_i18n( - engine: E, - template_name: String, - locale: LanguageIdentifier, - user_gender: Option, - user_formality: Option, - additional_context: minijinja::Value, -) -> impl IntoResponse { - // Contexte minimal - les traductions sont calculées à la demande - let template_context = template_context! { - locale => locale.to_string(), - user_gender => user_gender.as_ref().map(|g| g.as_str()).unwrap_or("neutral"), - user_formality => user_formality.as_ref().map(|f| f.as_str()).unwrap_or("informal"), - language => locale.language.as_str(), - region => locale.region.as_ref().map(|r| r.as_str()).unwrap_or(""), - ..additional_context - }; - - RenderHtml(template_name, engine, template_context) -} -``` - -### **3. Middleware HTMX pour propagation de langue** - -```rust -// middleware_i18n.rs -pub async fn htmx_language_middleware( - mut request: Request, - next: Next, -) -> Response { - let is_htmx = request.headers().get("HX-Request").is_some(); - - // Détection de langue pour HTMX - let locale = if is_htmx { - // 1. Header personnalisé HTMX - request.headers() - .get("HX-Current-Language") - .and_then(|h| h.to_str().ok()) - .and_then(|s| LanguageIdentifier::from_str(s).ok()) - } else { - None - }.or_else(|| { - // 2. Cookie session - extract_locale_from_cookies(&request) - }).or_else(|| { - // 3. Accept-Language - extract_locale_from_accept_language(&request) - }).unwrap_or_else(|| "en-us".parse().unwrap()); - - // Injecter dans extensions pour extraction ultérieure - request.extensions_mut().insert(Language(locale.clone())); - - let mut response = next.run(request).await; - - // Pour réponses HTMX, ajouter header de langue - if is_htmx { - response.headers_mut().insert( - "HX-Language", - HeaderValue::from_str(&locale.to_string()).unwrap() - ); - } - - response -} -``` - -### **4. Templates HTMX avec i18n intégrée** - -```html - -
- -
- - -
- - - - - -
-``` - -### **5. Handlers de formulaires HTMX** - -```rust -// handlers/profile.rs -pub async fn update_profile_htmx( - ctx: UserRequestContext, // Contient déjà la langue détectée - Form(form_data): Form, -) -> impl IntoResponse { - match validate_and_update_profile(&ctx, form_data).await { - Ok(updated_profile) => { - // Succès - renvoyer le contenu mis à jour - render_with_i18n( - ctx.web_context.engine.clone(), - "partials/profile_content.html".to_string(), - ctx.language.0, - ctx.auth.user_gender(), - ctx.auth.user_formality(), - template_context! { - profile => updated_profile, - success_message => "profile-updated-successfully" - } - ) - }, - Err(validation_errors) => { - // Erreur - renvoyer le formulaire avec erreurs - let mut response = render_with_i18n( - ctx.web_context.engine.clone(), - "partials/profile_form.html".to_string(), - ctx.language.0, - ctx.auth.user_gender(), - ctx.auth.user_formality(), - template_context! { - errors => validation_errors, - form_data => form_data - } - ).into_response(); - - // Status 422 pour indiquer erreur de validation - *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; - response - } - } -} -``` - -### **6. Partials avec traductions contextuelles** - -```html - -
-

{{ tgf(key="profile-title", locale=locale, gender=user_gender, formality=user_formality) }}

- - {% if success_message %} -
- {{ t(key=success_message, locale=locale) }} -
- {% endif %} - -
-

{{ t(key="display-name", locale=locale) }}: {{ profile.display_name }}

-

{{ t(key="handle", locale=locale) }}: @{{ profile.handle }}

-

{{ t(key="member-since", locale=locale) }}: {{ profile.created_at|date }}

-
- - -
-``` - -## **Avantages de cette approche pour HTMX** - -### **Performance optimale** -- ✅ **Calcul à la demande** : seules les traductions utilisées sont calculées -- ✅ **Pas de pré-rendu** : Élimination du HashMap de traductions -- ✅ **Cache Fluent natif** : Réutilisation du cache intégré - -### **Maintenance simplifiée** -- ✅ **Source unique** : fichiers .ftl sont la seule source de vérité -- ✅ **DRY** : plus de duplication de clés de traduction -- ✅ **Contexte propagé** : langue maintenue entre requêtes HTMX - -### **Robustesse HTMX** -- ✅ **Headers personnalisés** : langue propagée automatiquement -- ✅ **Fallbacks** : si langue manque, détection automatique -- ✅ **Validation d'erreurs** : messages traduits dans la bonne langue - -### **Flexibilité Romance** -- ✅ **Genre/formalité** : support complet dans les templates -- ✅ **Pluralisation** : gérée nativement par Fluent -- ✅ **Paramètres dynamiques** : variables calculées côté template - -Cette architecture est **parfaite pour HTMX** car elle évite le sur-calcul tout en maintenant la consistance linguistique entre les requêtes partielles ! - -gagnfra: Estce que le code de ce middleware est conforme à ta proposition ? - -GitHub Copilot: **Non, ce middleware n'est pas conforme à ma proposition HTMX.** Il y a plusieurs éléments manquants pour supporter correctement HTMX : - -## **Problèmes identifiés** - -### **1. Pas de support HTMX headers** -```rust -// ❌ Manquant : détection des headers HTMX -// Le middleware ne vérifie pas "HX-Current-Language" -``` - -### **2. Pas de middleware pour injection d'headers** -```rust -// ❌ Manquant : injection de "HX-Language" dans la réponse -// Actuellement c'est juste un extracteur, pas un vrai middleware -``` - -### **3. Ordre de priorité non-optimal pour HTMX** -```rust -// ❌ Ordre actuel : -// 1. Profil utilisateur -// 2. Cookie -// 3. Accept-Language -// 4. Défaut - -// ✅ Ordre optimal pour HTMX : -// 1. Header HX-Current-Language (priorité absolue) -// 2. Profil utilisateur -// 3. Cookie -// 4. Accept-Language -// 5. Défaut -``` - -## **Version corrigée conforme à ma proposition** - -### **1. Extracteur Language amélioré** -```rust -// middleware_i18n.rs -impl FromRequestParts for Language -where - WebContext: FromRef, - S: Send + Sync, -{ - type Rejection = Response; - - async fn from_request_parts(parts: &mut Parts, context: &S) -> Result { - trace!("Extracting Language from request"); - let web_context = WebContext::from_ref(context); - - // ✅ 1. PRIORITÉ ABSOLUE : Header HTMX (pour requêtes suivantes) - if let Some(htmx_lang) = parts.headers.get("HX-Current-Language") { - if let Ok(lang_str) = htmx_lang.to_str() { - if let Ok(locale) = LanguageIdentifier::from_str(lang_str) { - // Vérifier que la langue est supportée - for supported_lang in &web_context.i18n_context.supported_languages { - if supported_lang.matches(&locale, true, false) { - debug!(language = %supported_lang, "Using language from HTMX header"); - return Ok(Self(supported_lang.clone())); - } - } - } - } - } - - let auth: Auth = Cached::::from_request_parts(parts, context).await?.0; - - // ✅ 2. Profil utilisateur (si connecté) - if let Some(handle) = &auth.0 { - if let Ok(auth_lang) = handle.language.parse::() { - debug!(language = %auth_lang, "Using language from user profile"); - return Ok(Self(auth_lang)); - } - } - - // ✅ 3. Cookie de session - let cookie_jar = CookieJar::from_headers(&parts.headers); - if let Some(lang_cookie) = cookie_jar.get(COOKIE_LANG) { - // ... code existant inchangé - } - - // ✅ 4. Accept-Language header - // ... code existant inchangé - - // ✅ 5. Défaut - let default_lang = &web_context.i18n_context.supported_languages[0]; - debug!(language = %default_lang, "Using default language"); - Ok(Self(default_lang.clone())) - } -} -``` - -### **2. Vrai middleware pour HTMX** -```rust -// middleware_i18n.rs - Ajout nécessaire -use axum::{ - middleware::Next, - http::{Request, HeaderValue}, -}; - -/// Middleware pour gérer la propagation de langue avec HTMX -pub async fn htmx_language_middleware( - mut request: Request, - next: Next, -) -> axum::response::Response { - let is_htmx = request.headers().get("HX-Request").is_some(); - - // Traiter la requête - let response = next.run(request).await; - - // Pour les réponses HTMX, ajouter header de langue - if is_htmx { - // Récupérer la langue détectée (stockée par l'extracteur Language) - if let Some(extensions) = response.extensions().get::() { - let mut response = response; - response.headers_mut().insert( - "HX-Language", - HeaderValue::from_str(&extensions.0.to_string()).unwrap_or_default() - ); - return response; - } - } - - response -} -``` - -### **3. Fonction utilitaire pour détecter HTMX** -```rust -// middleware_i18n.rs - Helpers -pub fn is_htmx_request(parts: &Parts) -> bool { - parts.headers.get("HX-Request").is_some() -} - -pub fn extract_htmx_language(parts: &Parts) -> Option { - parts.headers - .get("HX-Current-Language")? - .to_str().ok()? - .parse().ok() -} -``` - -### **4. Integration dans l'app principale** -```rust -// Dans smokesignal.rs ou routes.rs -use crate::http::middleware_i18n::htmx_language_middleware; - -let app = Router::new() - .route("/profile", get(profile_handler)) - .route("/profile/update", post(update_profile_htmx)) - // ✅ Ajouter le middleware HTMX - .layer(middleware::from_fn(htmx_language_middleware)) - .with_state(web_context); -``` - -### **5. Templates avec propagation automatique** -```html - -
- - - -
-``` - -## **Résumé des améliorations nécessaires** - -1. ✅ **Ajouter détection HX-Current-Language** (priorité #1) -2. ✅ **Créer vrai middleware** pour injection headers -3. ✅ **Réorganiser l'ordre de priorité** pour HTMX -4. ✅ **Ajouter helpers** pour détecter requêtes HTMX -5. ✅ **Intégrer dans l'app** avec layer middleware - -Le code actuel est un bon **extracteur**, mais il manque la **couche middleware** pour une intégration HTMX complète ! - -gagnfra: explique moi quelle est la différence entre bare base et index - -GitHub Copilot: Voici l'explication de la hiérarchie des templates : - -## **Hiérarchie des templates** - -### **base.en-us.html** - Template de base complet -```html - - - - - - - - - {% include 'nav.en-us.html' %} - {% block content %}{% endblock %} - {% include 'footer.en-us.html' %} - - -``` - -**Usage** : Page complète avec ``, navigation, footer - ---- - -### **bare.en-us.html** - Template minimal sans HTML -```html -{% include 'nav.en-us.html' %} -{% block content %}{% endblock %} -{% include 'footer.en-us.html' %} -``` - -**Usage** : Fragment HTML pour **requêtes HTMX** (pas de ``, ``, etc.) - ---- - -### **index.en-us.html** - Page d'accueil complète -```html -{% extends "base.en-us.html" %} -{% block title %}Smoke Signal{% endblock %} -{% block head %} - - -{% endblock %} -{% block content %} - {% include 'index.en-us.common.html' %} -{% endblock %} -``` - -**Usage** : **Première visite** - page complète avec SEO - ---- - -### **index.en-us.bare.html** - Page d'accueil pour HTMX -```html -{% extends "bare.en-us.html" %} -{% block content %} - {% include 'index.en-us.common.html' %} -{% endblock %} -``` - -**Usage** : **Navigation HTMX** - même contenu, sans structure HTML complète - ---- - -### **index.en-us.common.html** - Contenu réutilisable -```html -
-

Smoke Signal

- -
-``` - -**Usage** : Contenu partagé entre version complète et HTMX - -## **Pattern d'utilisation** - -### **Première visite (URL directe)** -``` -GET / → index.en-us.html -└── extends base.en-us.html - ├── ... - ├── navigation - ├── index.en-us.common.html (contenu) - └── footer -``` - -### **Navigation HTMX (SPA-like)** -``` -GET / (avec HX-Request header) → index.en-us.bare.html -└── extends bare.en-us.html - ├── navigation (mise à jour) - ├── index.en-us.common.html (contenu) - └── footer (mise à jour) -``` - -### **Partials HTMX purs** -``` -POST /import → import.en-us.partial.html -└── Fragment HTML pur (juste le contenu à remplacer) -``` - -## **Avantages de cette architecture** - -### **Performance** -- ✅ **Première visite** : HTML complet avec SEO -- ✅ **Navigation** : Seulement le nécessaire via HTMX -- ✅ **Partials** : Fragments minimaux - -### **SEO & Accessibilité** -- ✅ **URLs directes** fonctionnent (pas de SPA brisé) -- ✅ **Meta tags** complets pour les moteurs de recherche -- ✅ **Navigation** fonctionne sans JavaScript - -### **Maintenabilité** -- ✅ **DRY** : contenu partagé via `.common.html` -- ✅ **Flexibilité** : même logique, différentes structures -- ✅ **Progressive enhancement** : fonctionne avec/sans HTMX - -## **Détection côté serveur** - -```rust -// Handler qui choisit le bon template -pub async fn index_handler( - ctx: UserRequestContext, - HxRequest(is_htmx): HxRequest, -) -> impl IntoResponse { - let template_name = if is_htmx { - "index.en-us.bare.html" // Fragment pour HTMX - } else { - "index.en-us.html" // Page complète - }; - - render_with_i18n( - ctx.web_context.engine.clone(), - template_name.to_string(), - ctx.language.0, - // ... - ) -} -``` - -Cette architecture permet une **expérience SPA fluide** tout en gardant un **site web traditionnel fonctionnel** ! - -gagnfra: donc si on suit la méthode mentionnée, avec ta proposition de passer le language via middleware les requete HTMX m'éviterons de pré-rendre le contenu traduit de fluent - -GitHub Copilot: **Exactement !** Avec ma proposition de middleware HTMX + fonctions i18n intégrées, vous **éliminez complètement le pré-rendu** et améliorez drastiquement les performances. - -## **Transformation : Avant vs Après** - -### **❌ Avant (code actuel)** -```rust -// 20+ traductions pré-calculées pour CHAQUE rendu -translations.insert("profile-greeting".to_string(), i18n_context.tgf("profile-greeting", None, None, None)); -translations.insert("first-visit-title".to_string(), i18n_context.tgf("first-visit-title", None, None, None)); -translations.insert("actions".to_string(), i18n_context.t("actions", None)); -// ... 17 autres traductions calculées même si pas utilisées -``` - -### **✅ Après (ma proposition)** -```rust -// Contexte minimal - traductions calculées à la demande -let template_context = template_context! { - locale => locale.to_string(), - user_gender => user_gender_str, - user_formality => user_formality_str, - ..additional_context -}; -// Fini le HashMap de traductions ! -``` - -## **Templates transformés** - -### **❌ Avant** -```html - -

{{ translations.profile_greeting }}

- -

{{ translations.member_since }}

-``` - -### **✅ Après** -```html - -

{{ tgf(key="profile-greeting", locale=locale, gender=user_gender, formality=user_formality) }}

- -

{{ t(key="member-since", locale=locale) }}

-``` - -## **Middleware HTMX pour propagation automatique** - -```rust -// La langue est détectée UNE FOIS par le middleware -pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { - // Détection automatique de la langue (ordre optimal) - let locale = detect_language_for_htmx(&request); - - // Injection dans request pour extracteur Language - request.extensions_mut().insert(Language(locale.clone())); - - let mut response = next.run(request).await; - - // Pour HTMX, ajouter header de propagation - if is_htmx_request(&request) { - response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); - } - - response -} -``` - -## **Gain de performance massif** - -### **Calculs évités par requête HTMX** -- ❌ **20+ traductions** calculées systématiquement -- ❌ **HashMap** de 20+ entrées créé -- ❌ **Clonage** de toutes les traductions -- ❌ **Extraction** de paramètres non utilisés - -### **Calculs à la demande** -- ✅ **Seulement** les traductions utilisées dans le template -- ✅ **Cache Fluent** réutilisé automatiquement -- ✅ **Contexte minimal** dans les templates -- ✅ **Langue propagée** automatiquement - -## **Exemple concret avec formulaire HTMX** - -### **Template optimisé** -```html - -
-
- - -
- - -
-``` - -### **Handler simplifié** -```rust -pub async fn update_profile_htmx( - ctx: UserRequestContext, // Langue déjà détectée par middleware - Form(form_data): Form, -) -> impl IntoResponse { - // Plus besoin de passer locales Arc, gender, formality séparément ! - render_with_i18n_optimized( - ctx.web_context.engine.clone(), - "partials/profile_form.html".to_string(), - ctx.language.0, // Détecté automatiquement (HTMX header -> cookie -> accept-language) - template_context! { - form_data => form_data, - errors => validation_errors - } - ) -} -``` - -## **Résultat final** - -### **Performance** -- 🚀 **~80% moins de calculs** par requête HTMX -- 🚀 **Mémoire optimisée** (pas de HashMap de traductions) -- 🚀 **Cache Fluent natif** exploité - -### **Maintenabilité** -- 🎯 **Source unique** : fichiers .ftl -- 🎯 **DRY** : plus de duplication de clés -- 🎯 **Templates plus lisibles** - -### **Robustesse HTMX** -- 🔄 **Langue maintenue** entre requêtes -- 🔄 **Fallbacks automatiques** si détection échoue -- 🔄 **Headers propagés** transparents - -**Votre intuition est parfaite** : cette approche élimine le goulot d'étranglement du pré-rendu tout en améliorant l'expérience HTMX ! - -gagnfra: Parfait. Maintenant, fait un document de directives basé sur notre discussion destiné à un agent claude 4. soit détaillé mais concis sur les méthodes de développement à suivre (entre autres de garder les commentaires en anglais) pour revamper le i18n. nous partirons d'une version qui n'aura aucune intégration i18n. - -Je te fournis un exemple de ce que je veux mais avec notre projet. - -GitHub Copilot: # Smokesignal I18n Development Guidelines - -This document provides guidance for implementing comprehensive internationalization (i18n) in the Smokesignal web application using modern Rust patterns and HTMX integration. - -## Project Overview - -Smokesignal is a Rust web application built with Axum that requires full internationalization support for multiple languages with advanced features including gender agreement and formality levels for Romance languages. - -## I18n Architecture Goals - -- **HTMX-first design**: Seamless language propagation across partial page updates -- **Performance-optimized**: On-demand translation calculation instead of pre-rendering -- **Romance language support**: Gender agreement and formality levels (tu/vous) -- **Fluent-based**: Mozilla Fluent for sophisticated translation features -- **Template integration**: Direct i18n functions in Jinja2 templates - -## Core Modules Structure - -``` -src/i18n/ -├── mod.rs # Main i18n exports and Locales struct -├── errors.rs # Structured error types for i18n operations -├── fluent_loader.rs # Fluent file loading (embed vs reload modes) -└── template_helpers.rs # Template function integration - -src/http/ -├── middleware_i18n.rs # HTMX-aware language detection middleware -├── template_i18n.rs # Template context with gender/formality support -└── templates.rs # Template rendering with integrated i18n functions -``` - -## Language Detection Priority - -Implement language detection with this exact priority order for HTMX compatibility: - -1. **HX-Current-Language header** (highest priority for HTMX requests) -2. **User profile language** (if authenticated) -3. **lang cookie** (session preference) -4. **Accept-Language header** (browser preference) -5. **Default language** (fallback) - -## Template Integration Pattern - -Replace pre-rendered translation HashMap with direct template functions: - -### ❌ Avoid (pre-rendering approach) -```rust -// Don't pre-calculate all translations -let mut translations = HashMap::new(); -translations.insert("profile-greeting".to_string(), i18n_context.tgf(...)); -``` - -### ✅ Use (on-demand functions) -```rust -// Register i18n functions in template engine -env.add_function("t", |args| { /* basic translation */ }); -env.add_function("tgf", |args| { /* gender + formality */ }); -env.add_function("tc", |args| { /* count-based pluralization */ }); -``` - -### Template Usage -```html - -

{{ tgf(key="profile-greeting", locale=locale, gender=user_gender, formality=user_formality) }}

- -

{{ tc(key="events-created", locale=locale, count=event_count) }}

-``` - -## HTMX Integration Requirements - -### Middleware Implementation -```rust -pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { - let is_htmx = request.headers().get("HX-Request").is_some(); - - // Detect language with HTMX priority - let locale = detect_language_with_htmx_priority(&request); - - // Inject into request extensions - request.extensions_mut().insert(Language(locale.clone())); - - let mut response = next.run(request).await; - - // Add language propagation header for HTMX - if is_htmx { - response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); - } - - response -} -``` - -### Template Structure for HTMX -Support both full page loads and HTMX partials: -``` -templates/ -├── index.en-us.html # Full page (first visit) -├── index.en-us.bare.html # HTMX navigation (no ) -├── index.en-us.common.html # Shared content -└── partials/ - └── form.en-us.html # HTMX form fragments -``` - -## Error Handling - -All i18n error strings must follow this format: -``` -error-smokesignal-i18n-- :
-``` - -Example errors: -``` -error-smokesignal-i18n-fluent-1 Translation key not found: profile-greeting -error-smokesignal-i18n-locale-2 Unsupported language identifier: xx-XX -error-smokesignal-i18n-template-3 Template function argument missing: locale -``` - -Use structured error enums with `thiserror`: -```rust -#[derive(Debug, Error)] -pub enum I18nError { - #[error("error-smokesignal-i18n-fluent-1 Translation key not found: {key}")] - TranslationKeyNotFound { key: String }, - - #[error("error-smokesignal-i18n-locale-2 Unsupported language identifier: {locale}")] - UnsupportedLocale { locale: String }, -} -``` - -## Configuration Management - -### Feature Flags -```toml -[features] -default = ["embed"] -embed = ["minijinja-embed"] # Production: templates in binary -reload = ["minijinja-autoreload"] # Development: hot reload -``` - -### Supported Languages -```rust -pub const SUPPORTED_LANGUAGES: &[&str] = &["en-us", "fr-ca"]; - -pub fn create_supported_languages() -> Vec { - SUPPORTED_LANGUAGES.iter() - .map(|lang| LanguageIdentifier::from_str(lang).unwrap()) - .collect() -} -``` - -## Fluent File Organization - -``` -i18n/ -├── en-us/ -│ ├── common.ftl # Shared UI elements -│ ├── errors.ftl # Error messages -│ └── ui.ftl # Interface text -└── fr-ca/ - ├── common.ftl - ├── errors.ftl - └── ui.ftl -``` - -### Fluent Syntax Examples -```ftl -# Gender and formality variants -profile-greeting = Hello -profile-greeting-feminine = Hello miss -profile-greeting-masculine = Hello sir -profile-greeting-feminine-formal = Good day madam -profile-greeting-masculine-formal = Good day sir - -# Count-based pluralization -events-created = { $count -> - [0] No events created - [1] One event created - *[other] {$count} events created -} - -# Parameterized messages -welcome-user = Welcome {$name}! -``` - -## Performance Guidelines - -### ✅ Do -- Use on-demand translation calculation -- Leverage Fluent's built-in caching -- Register template functions once at startup -- Minimal template context (just locale info) - -### ❌ Don't -- Pre-render translation HashMaps -- Clone translation data unnecessarily -- Load all translations for every request -- Use `println!` for debugging (use `tracing::debug!`) - -## Testing Requirements - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_language_detection_htmx_priority() { - // Test HX-Current-Language header takes priority - } - - #[test] - fn test_template_function_basic_translation() { - // Test t() function works correctly - } - - #[test] - fn test_gender_formality_combinations() { - // Test tgf() with all gender/formality combinations - } -} -``` - -## Logging - -Use structured logging with `tracing`: -```rust -tracing::debug!(locale = %locale, "Language detected for request"); -tracing::trace!(key = %key, locale = %locale, "Translation requested"); -``` - -Instrument async functions: -```rust -#[tracing::instrument(skip(locales))] -pub async fn load_translations(locales: &mut Locales) -> Result<()> { - // Implementation -} -``` - -## Code Comments - -Keep all code comments in English: -```rust -// Create i18n context with user-specific gender and formality preferences -let i18n_context = TemplateI18nContext::new(locale, locales) - .with_gender(user_gender.unwrap_or(Gender::Neutral)) - .with_formality(user_formality.unwrap_or(Formality::Informal)); -``` - -## Migration Strategy - -When starting from a version with no i18n integration: - -1. **Phase 1**: Implement core `i18n` module with Fluent loading -2. **Phase 2**: Add language detection middleware with HTMX support -3. **Phase 3**: Integrate template functions and remove hardcoded strings -4. **Phase 4**: Add gender/formality support for Romance languages -5. **Phase 5**: Implement template hierarchy (base/bare/common) for HTMX - -Each phase should be fully tested and deployable independently. - -gagnfra: remove formality, we wont use it. but add gender neutral. - -GitHub Copilot: # Smokesignal I18n Development Guidelines - -This document provides guidance for implementing comprehensive internationalization (i18n) in the Smokesignal web application using modern Rust patterns and HTMX integration. - -## Project Overview - -Smokesignal is a Rust web application built with Axum that requires full internationalization support for multiple languages with advanced features including gender agreement for Romance languages. - -## I18n Architecture Goals - -- **HTMX-first design**: Seamless language propagation across partial page updates -- **Performance-optimized**: On-demand translation calculation instead of pre-rendering -- **Romance language support**: Gender agreement (masculine/feminine/neutral) -- **Fluent-based**: Mozilla Fluent for sophisticated translation features -- **Template integration**: Direct i18n functions in Jinja2 templates - -## Core Modules Structure - -``` -src/i18n/ -├── mod.rs # Main i18n exports and Locales struct -├── errors.rs # Structured error types for i18n operations -├── fluent_loader.rs # Fluent file loading (embed vs reload modes) -└── template_helpers.rs # Template function integration - -src/http/ -├── middleware_i18n.rs # HTMX-aware language detection middleware -├── template_i18n.rs # Template context with gender support -└── templates.rs # Template rendering with integrated i18n functions -``` - -## Language Detection Priority - -Implement language detection with this exact priority order for HTMX compatibility: - -1. **HX-Current-Language header** (highest priority for HTMX requests) -2. **User profile language** (if authenticated) -3. **lang cookie** (session preference) -4. **Accept-Language header** (browser preference) -5. **Default language** (fallback) - -## Template Integration Pattern - -Replace pre-rendered translation HashMap with direct template functions: - -### ❌ Avoid (pre-rendering approach) -```rust -// Don't pre-calculate all translations -let mut translations = HashMap::new(); -translations.insert("profile-greeting".to_string(), i18n_context.tg(...)); -``` - -### ✅ Use (on-demand functions) -```rust -// Register i18n functions in template engine -env.add_function("t", |args| { /* basic translation */ }); -env.add_function("tg", |args| { /* gender-aware translation */ }); -env.add_function("tc", |args| { /* count-based pluralization */ }); -``` - -### Template Usage -```html - -

{{ tg(key="profile-greeting", locale=locale, gender=user_gender) }}

- -

{{ tc(key="events-created", locale=locale, count=event_count) }}

-``` - -## HTMX Integration Requirements - -### Middleware Implementation -```rust -pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { - let is_htmx = request.headers().get("HX-Request").is_some(); - - // Detect language with HTMX priority - let locale = detect_language_with_htmx_priority(&request); - - // Inject into request extensions - request.extensions_mut().insert(Language(locale.clone())); - - let mut response = next.run(request).await; - - // Add language propagation header for HTMX - if is_htmx { - response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); - } - - response -} -``` - -### Template Structure for HTMX -Support both full page loads and HTMX partials: -``` -templates/ -├── index.en-us.html # Full page (first visit) -├── index.en-us.bare.html # HTMX navigation (no ) -├── index.en-us.common.html # Shared content -└── partials/ - └── form.en-us.html # HTMX form fragments -``` - -## Error Handling - -All i18n error strings must follow this format: -``` -error-smokesignal-i18n-- :
-``` - -Example errors: -``` -error-smokesignal-i18n-fluent-1 Translation key not found: profile-greeting -error-smokesignal-i18n-locale-2 Unsupported language identifier: xx-XX -error-smokesignal-i18n-template-3 Template function argument missing: locale -``` - -Use structured error enums with `thiserror`: -```rust -#[derive(Debug, Error)] -pub enum I18nError { - #[error("error-smokesignal-i18n-fluent-1 Translation key not found: {key}")] - TranslationKeyNotFound { key: String }, - - #[error("error-smokesignal-i18n-locale-2 Unsupported language identifier: {locale}")] - UnsupportedLocale { locale: String }, -} -``` - -## Gender Support - -### Gender Enum -```rust -#[derive(Debug, Clone)] -pub enum Gender { - Masculine, - Feminine, - Neutral, -} - -impl Gender { - pub fn as_str(&self) -> &'static str { - match self { - Gender::Masculine => "masculine", - Gender::Feminine => "feminine", - Gender::Neutral => "neutral", - } - } -} -``` - -### Template Context -```rust -// Template context includes gender information -let template_context = template_context! { - locale => locale.to_string(), - user_gender => user_gender.as_ref().map(|g| g.as_str()).unwrap_or("neutral"), - ..additional_context -}; -``` - -## Configuration Management - -### Feature Flags -```toml -[features] -default = ["embed"] -embed = ["minijinja-embed"] # Production: templates in binary -reload = ["minijinja-autoreload"] # Development: hot reload -``` - -### Supported Languages -```rust -pub const SUPPORTED_LANGUAGES: &[&str] = &["en-us", "fr-ca"]; - -pub fn create_supported_languages() -> Vec { - SUPPORTED_LANGUAGES.iter() - .map(|lang| LanguageIdentifier::from_str(lang).unwrap()) - .collect() -} -``` - -## Fluent File Organization - -``` -i18n/ -├── en-us/ -│ ├── common.ftl # Shared UI elements -│ ├── errors.ftl # Error messages -│ └── ui.ftl # Interface text -└── fr-ca/ - ├── common.ftl - ├── errors.ftl - └── ui.ftl -``` - -### Fluent Syntax Examples -```ftl -# Gender variants -profile-greeting = Hello -profile-greeting-feminine = Hello miss -profile-greeting-masculine = Hello sir -profile-greeting-neutral = Hello there - -welcome-message = Welcome! -welcome-message-feminine = Bienvenue! -welcome-message-masculine = Bienvenu! -welcome-message-neutral = Bienvenue! - -# Count-based pluralization -events-created = { $count -> - [0] No events created - [1] One event created - *[other] {$count} events created -} - -# Parameterized messages -welcome-user = Welcome {$name}! -``` - -## Performance Guidelines - -### ✅ Do -- Use on-demand translation calculation -- Leverage Fluent's built-in caching -- Register template functions once at startup -- Minimal template context (just locale and gender info) - -### ❌ Don't -- Pre-render translation HashMaps -- Clone translation data unnecessarily -- Load all translations for every request -- Use `println!` for debugging (use `tracing::debug!`) - -## Testing Requirements - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_language_detection_htmx_priority() { - // Test HX-Current-Language header takes priority - } - - #[test] - fn test_template_function_basic_translation() { - // Test t() function works correctly - } - - #[test] - fn test_gender_variants() { - // Test tg() with all gender combinations (masculine/feminine/neutral) - } -} -``` - -## Logging - -Use structured logging with `tracing`: -```rust -tracing::debug!(locale = %locale, "Language detected for request"); -tracing::trace!(key = %key, locale = %locale, "Translation requested"); -``` - -Instrument async functions: -```rust -#[tracing::instrument(skip(locales))] -pub async fn load_translations(locales: &mut Locales) -> Result<()> { - // Implementation -} -``` - -## Code Comments - -Keep all code comments in English: -```rust -// Create i18n context with user-specific gender preferences -let i18n_context = TemplateI18nContext::new(locale, locales) - .with_gender(user_gender.unwrap_or(Gender::Neutral)); -``` - -## Migration Strategy - -When starting from a version with no i18n integration: - -1. **Phase 1**: Implement core `i18n` module with Fluent loading -2. **Phase 2**: Add language detection middleware with HTMX support -3. **Phase 3**: Integrate template functions and remove hardcoded strings -4. **Phase 4**: Add gender support for Romance languages -5. **Phase 5**: Implement template hierarchy (base/bare/common) for HTMX - -Each phase should be fully tested and deployable independently. \ No newline at end of file diff --git a/docs/i18n_module_summary.md b/docs/i18n_module_summary.md new file mode 100644 index 0000000..96a7cea --- /dev/null +++ b/docs/i18n_module_summary.md @@ -0,0 +1,556 @@ +# Smokesignal I18n Development Guidelines + +This document provides guidance for implementing comprehensive internationalization (i18n) in the Smokesignal web application using modern Rust patterns and HTMX integration. + +## Project Overview + +Smokesignal is a Rust web application built with Axum that requires full internationalization support for multiple languages with advanced features including gender agreement and formality levels for Romance languages. + +## I18n Architecture Goals + +- **HTMX-first design**: Seamless language propagation across partial page updates +- **Performance-optimized**: On-demand translation calculation instead of pre-rendering +- **Romance language support**: Gender agreement and formality levels (tu/vous) +- **Fluent-based**: Mozilla Fluent for sophisticated translation features +- **Template integration**: Direct i18n functions in Jinja2 templates + +## Core Modules Structure + +``` +src/i18n/ +├── mod.rs # Main i18n exports and Locales struct +├── errors.rs # Structured error types for i18n operations +├── fluent_loader.rs # Fluent file loading (embed vs reload modes) +└── template_helpers.rs # Template function integration + +src/http/ +├── middleware_i18n.rs # HTMX-aware language detection middleware +├── template_i18n.rs # Template context with gender/formality support +└── templates.rs # Template rendering with integrated i18n functions +``` + +## Language Detection Priority + +Implement language detection with this exact priority order for HTMX compatibility: + +1. **HX-Current-Language header** (highest priority for HTMX requests) +2. **User profile language** (if authenticated) +3. **lang cookie** (session preference) +4. **Accept-Language header** (browser preference) +5. **Default language** (fallback) + +## Template Integration Pattern + +Replace pre-rendered translation HashMap with direct template functions: + +### ❌ Avoid (pre-rendering approach) +```rust +// Don't pre-calculate all translations +let mut translations = HashMap::new(); +translations.insert("profile-greeting".to_string(), i18n_context.tgf(...)); +``` + +### ✅ Use (on-demand functions) +```rust +// Register i18n functions in template engine +env.add_function("t", |args| { /* basic translation */ }); +env.add_function("tgf", |args| { /* gender + formality */ }); +env.add_function("tc", |args| { /* count-based pluralization */ }); +``` + +### Template Usage +```html + +

{{ tgf(key="profile-greeting", locale=locale, gender=user_gender, formality=user_formality) }}

+ +

{{ tc(key="events-created", locale=locale, count=event_count) }}

+``` + +## HTMX Integration Requirements + +### Middleware Implementation +```rust +pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { + let is_htmx = request.headers().get("HX-Request").is_some(); + + // Detect language with HTMX priority + let locale = detect_language_with_htmx_priority(&request); + + // Inject into request extensions + request.extensions_mut().insert(Language(locale.clone())); + + let mut response = next.run(request).await; + + // Add language propagation header for HTMX + if is_htmx { + response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); + } + + response +} +``` + +### Template Structure for HTMX +Support both full page loads and HTMX partials: +``` +templates/ +├── index.en-us.html # Full page (first visit) +├── index.en-us.bare.html # HTMX navigation (no ) +├── index.en-us.common.html # Shared content +└── partials/ + └── form.en-us.html # HTMX form fragments +``` + +## Error Handling + +All i18n error strings must follow this format: +``` +error-smokesignal-i18n-- :
+``` + +Example errors: +``` +error-smokesignal-i18n-fluent-1 Translation key not found: profile-greeting +error-smokesignal-i18n-locale-2 Unsupported language identifier: xx-XX +error-smokesignal-i18n-template-3 Template function argument missing: locale +``` + +Use structured error enums with `thiserror`: +```rust +#[derive(Debug, Error)] +pub enum I18nError { + #[error("error-smokesignal-i18n-fluent-1 Translation key not found: {key}")] + TranslationKeyNotFound { key: String }, + + #[error("error-smokesignal-i18n-locale-2 Unsupported language identifier: {locale}")] + UnsupportedLocale { locale: String }, +} +``` + +## Configuration Management + +### Feature Flags +```toml +[features] +default = ["embed"] +embed = ["minijinja-embed"] # Production: templates in binary +reload = ["minijinja-autoreload"] # Development: hot reload +``` + +### Supported Languages +```rust +pub const SUPPORTED_LANGUAGES: &[&str] = &["en-us", "fr-ca"]; + +pub fn create_supported_languages() -> Vec { + SUPPORTED_LANGUAGES.iter() + .map(|lang| LanguageIdentifier::from_str(lang).unwrap()) + .collect() +} +``` + +## Fluent File Organization + +``` +i18n/ +├── en-us/ +│ ├── common.ftl # Shared UI elements +│ ├── errors.ftl # Error messages +│ └── ui.ftl # Interface text +└── fr-ca/ + ├── common.ftl + ├── errors.ftl + └── ui.ftl +``` + +### Fluent Syntax Examples +```ftl +# Gender and formality variants +profile-greeting = Hello +profile-greeting-feminine = Hello miss +profile-greeting-masculine = Hello sir +profile-greeting-feminine-formal = Good day madam +profile-greeting-masculine-formal = Good day sir + +# Count-based pluralization +events-created = { $count -> + [0] No events created + [1] One event created + *[other] {$count} events created +} + +# Parameterized messages +welcome-user = Welcome {$name}! +``` + +## Performance Guidelines + +### ✅ Do +- Use on-demand translation calculation +- Leverage Fluent's built-in caching +- Register template functions once at startup +- Minimal template context (just locale info) + +### ❌ Don't +- Pre-render translation HashMaps +- Clone translation data unnecessarily +- Load all translations for every request +- Use `println!` for debugging (use `tracing::debug!`) + +## Testing Requirements + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_language_detection_htmx_priority() { + // Test HX-Current-Language header takes priority + } + + #[test] + fn test_template_function_basic_translation() { + // Test t() function works correctly + } + + #[test] + fn test_gender_formality_combinations() { + // Test tgf() with all gender/formality combinations + } +} +``` + +## Logging + +Use structured logging with `tracing`: +```rust +tracing::debug!(locale = %locale, "Language detected for request"); +tracing::trace!(key = %key, locale = %locale, "Translation requested"); +``` + +Instrument async functions: +```rust +#[tracing::instrument(skip(locales))] +pub async fn load_translations(locales: &mut Locales) -> Result<()> { + // Implementation +} +``` + +## Code Comments + +Keep all code comments in English: +```rust +// Create i18n context with user-specific gender and formality preferences +let i18n_context = TemplateI18nContext::new(locale, locales) + .with_gender(user_gender.unwrap_or(Gender::Neutral)) + .with_formality(user_formality.unwrap_or(Formality::Informal)); +``` + +## Migration Strategy + +When starting from a version with no i18n integration: + +1. **Phase 1**: Implement core `i18n` module with Fluent loading +2. **Phase 2**: Add language detection middleware with HTMX support +3. **Phase 3**: Integrate template functions and remove hardcoded strings +4. **Phase 4**: Add gender/formality support for Romance languages +5. **Phase 5**: Implement template hierarchy (base/bare/common) for HTMX + +Each phase should be fully tested and deployable independently. + +gagnfra: remove formality, we wont use it. but add gender neutral. + +GitHub Copilot: # Smokesignal I18n Development Guidelines + +This document provides guidance for implementing comprehensive internationalization (i18n) in the Smokesignal web application using modern Rust patterns and HTMX integration. + +## Project Overview + +Smokesignal is a Rust web application built with Axum that requires full internationalization support for multiple languages with advanced features including gender agreement for Romance languages. + +## I18n Architecture Goals + +- **HTMX-first design**: Seamless language propagation across partial page updates +- **Performance-optimized**: On-demand translation calculation instead of pre-rendering +- **Romance language support**: Gender agreement (masculine/feminine/neutral) +- **Fluent-based**: Mozilla Fluent for sophisticated translation features +- **Template integration**: Direct i18n functions in Jinja2 templates + +## Core Modules Structure + +``` +src/i18n/ +├── mod.rs # Main i18n exports and Locales struct +├── errors.rs # Structured error types for i18n operations +├── fluent_loader.rs # Fluent file loading (embed vs reload modes) +└── template_helpers.rs # Template function integration + +src/http/ +├── middleware_i18n.rs # HTMX-aware language detection middleware +├── template_i18n.rs # Template context with gender support +└── templates.rs # Template rendering with integrated i18n functions +``` + +## Language Detection Priority + +Implement language detection with this exact priority order for HTMX compatibility: + +1. **HX-Current-Language header** (highest priority for HTMX requests) +2. **User profile language** (if authenticated) +3. **lang cookie** (session preference) +4. **Accept-Language header** (browser preference) +5. **Default language** (fallback) + +## Template Integration Pattern + +Replace pre-rendered translation HashMap with direct template functions: + +### ❌ Avoid (pre-rendering approach) +```rust +// Don't pre-calculate all translations +let mut translations = HashMap::new(); +translations.insert("profile-greeting".to_string(), i18n_context.tg(...)); +``` + +### ✅ Use (on-demand functions) +```rust +// Register i18n functions in template engine +env.add_function("t", |args| { /* basic translation */ }); +env.add_function("tg", |args| { /* gender-aware translation */ }); +env.add_function("tc", |args| { /* count-based pluralization */ }); +``` + +### Template Usage +```html + +

{{ tg(key="profile-greeting", locale=locale, gender=user_gender) }}

+ +

{{ tc(key="events-created", locale=locale, count=event_count) }}

+``` + +## HTMX Integration Requirements + +### Middleware Implementation +```rust +pub async fn htmx_language_middleware(request: Request, next: Next) -> Response { + let is_htmx = request.headers().get("HX-Request").is_some(); + + // Detect language with HTMX priority + let locale = detect_language_with_htmx_priority(&request); + + // Inject into request extensions + request.extensions_mut().insert(Language(locale.clone())); + + let mut response = next.run(request).await; + + // Add language propagation header for HTMX + if is_htmx { + response.headers_mut().insert("HX-Language", locale.to_string().parse().unwrap()); + } + + response +} +``` + +### Template Structure for HTMX +Support both full page loads and HTMX partials: +``` +templates/ +├── index.en-us.html # Full page (first visit) +├── index.en-us.bare.html # HTMX navigation (no ) +├── index.en-us.common.html # Shared content +└── partials/ + └── form.en-us.html # HTMX form fragments +``` + +## Error Handling + +All i18n error strings must follow this format: +``` +error-smokesignal-i18n-- :
+``` + +Example errors: +``` +error-smokesignal-i18n-fluent-1 Translation key not found: profile-greeting +error-smokesignal-i18n-locale-2 Unsupported language identifier: xx-XX +error-smokesignal-i18n-template-3 Template function argument missing: locale +``` + +Use structured error enums with `thiserror`: +```rust +#[derive(Debug, Error)] +pub enum I18nError { + #[error("error-smokesignal-i18n-fluent-1 Translation key not found: {key}")] + TranslationKeyNotFound { key: String }, + + #[error("error-smokesignal-i18n-locale-2 Unsupported language identifier: {locale}")] + UnsupportedLocale { locale: String }, +} +``` + +## Gender Support + +### Gender Enum +```rust +#[derive(Debug, Clone)] +pub enum Gender { + Masculine, + Feminine, + Neutral, +} + +impl Gender { + pub fn as_str(&self) -> &'static str { + match self { + Gender::Masculine => "masculine", + Gender::Feminine => "feminine", + Gender::Neutral => "neutral", + } + } +} +``` + +### Template Context +```rust +// Template context includes gender information +let template_context = template_context! { + locale => locale.to_string(), + user_gender => user_gender.as_ref().map(|g| g.as_str()).unwrap_or("neutral"), + ..additional_context +}; +``` + +## Configuration Management + +### Feature Flags +```toml +[features] +default = ["embed"] +embed = ["minijinja-embed"] # Production: templates in binary +reload = ["minijinja-autoreload"] # Development: hot reload +``` + +### Supported Languages +```rust +pub const SUPPORTED_LANGUAGES: &[&str] = &["en-us", "fr-ca"]; + +pub fn create_supported_languages() -> Vec { + SUPPORTED_LANGUAGES.iter() + .map(|lang| LanguageIdentifier::from_str(lang).unwrap()) + .collect() +} +``` + +## Fluent File Organization + +``` +i18n/ +├── en-us/ +│ ├── common.ftl # Shared UI elements +│ ├── errors.ftl # Error messages +│ └── ui.ftl # Interface text +└── fr-ca/ + ├── common.ftl + ├── errors.ftl + └── ui.ftl +``` + +### Fluent Syntax Examples +```ftl +# Gender variants +profile-greeting = Hello +profile-greeting-feminine = Hello miss +profile-greeting-masculine = Hello sir +profile-greeting-neutral = Hello there + +welcome-message = Welcome! +welcome-message-feminine = Bienvenue! +welcome-message-masculine = Bienvenu! +welcome-message-neutral = Bienvenue! + +# Count-based pluralization +events-created = { $count -> + [0] No events created + [1] One event created + *[other] {$count} events created +} + +# Parameterized messages +welcome-user = Welcome {$name}! +``` + +## Performance Guidelines + +### ✅ Do +- Use on-demand translation calculation +- Leverage Fluent's built-in caching +- Register template functions once at startup +- Minimal template context (just locale and gender info) + +### ❌ Don't +- Pre-render translation HashMaps +- Clone translation data unnecessarily +- Load all translations for every request +- Use `println!` for debugging (use `tracing::debug!`) + +## Testing Requirements + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_language_detection_htmx_priority() { + // Test HX-Current-Language header takes priority + } + + #[test] + fn test_template_function_basic_translation() { + // Test t() function works correctly + } + + #[test] + fn test_gender_variants() { + // Test tg() with all gender combinations (masculine/feminine/neutral) + } +} +``` + +## Logging + +Use structured logging with `tracing`: +```rust +tracing::debug!(locale = %locale, "Language detected for request"); +tracing::trace!(key = %key, locale = %locale, "Translation requested"); +``` + +Instrument async functions: +```rust +#[tracing::instrument(skip(locales))] +pub async fn load_translations(locales: &mut Locales) -> Result<()> { + // Implementation +} +``` + +## Code Comments + +Keep all code comments in English: +```rust +// Create i18n context with user-specific gender preferences +let i18n_context = TemplateI18nContext::new(locale, locales) + .with_gender(user_gender.unwrap_or(Gender::Neutral)); +``` + +## Migration Strategy + +When starting from a version with no i18n integration: + +1. **Phase 1**: Implement core `i18n` module with Fluent loading +2. **Phase 2**: Add language detection middleware with HTMX support +3. **Phase 3**: Integrate template functions and remove hardcoded strings +4. **Phase 4**: Add gender support for Romance languages +5. **Phase 5**: Implement template hierarchy (base/bare/common) for HTMX + +Each phase should be fully tested and deployable independently. \ No newline at end of file diff --git a/i18n_cleanup_reference.md b/i18n_cleanup_reference.md new file mode 100644 index 0000000..e69de29 diff --git a/i18n_rust_testing_summary.md b/i18n_rust_testing_summary.md new file mode 100644 index 0000000..e69de29