i18n+filtering fork - fluent-templates v2

fix: Use proper handle validation for organizer display names

Replace hardcoded DID truncation with handle validation logic in EventView
creation. Fixes organizer names showing as "@did:plc:hjvw..." instead of
proper handles like "@saving-kit.pyroclastic.cloud" in event listings.

-7
i18n/en-us/actions.ftl
··· 61 61 # Status options for events 62 62 status-active = Active 63 63 64 - # Status options for RSVPs 65 - status-going = Going 66 - status-interested = Interested 67 - status-not-going = Not Going 68 - 69 - # Event modes 70 - 71 64 # Location types 72 65 location-type-venue = Venue 73 66 location-type-coordinates = Coordinates
+8
i18n/en-us/ui.ftl
··· 333 333 community-lexicon-calendar-event-status-postponed = Postponed 334 334 community-lexicon-calendar-event-status-planned = Planned 335 335 336 + # User Status Labels (for RSVP and role indicators) 337 + status-organizer = Organizer 338 + status-legacy = Legacy 339 + status-going = Going 340 + status-interested = Interested 341 + status-not-going = Not Going 342 + status-unknown = Unknown 343 + 336 344 # Distance and Location 337 345 distance-km = {$distance} km away 338 346 location-unknown = Location not specified
-6
i18n/fr-ca/actions.ftl
··· 61 61 # Options de statut pour les événements 62 62 status-active = Actif 63 63 64 - # Options de statut pour les RSVP 65 - status-going = J'y vais 66 - status-interested = Intéressé 67 - status-not-going = Je n'y vais pas 68 - 69 - # Modes d'événement 70 64 71 65 # Types d'emplacement 72 66 location-type-venue = Lieu
+8
i18n/fr-ca/ui.ftl
··· 333 333 community-lexicon-calendar-event-status-postponed = Reporté 334 334 community-lexicon-calendar-event-status-planned = Planifié 335 335 336 + # Étiquettes de statut utilisateur (pour les indicateurs de RSVP et de rôle) 337 + status-organizer = Organisateur 338 + status-legacy = Ancienne version 339 + status-going = Je participe 340 + status-interested = Intéressé 341 + status-not-going = Je n'y vais pas 342 + status-unknown = Inconnu 343 + 336 344 # Distance et lieu 337 345 distance-km = {$distance} km de distance 338 346 location-unknown = Lieu non spécifié
+17 -103
src/filtering/facets.rs
··· 344 344 // Define date ranges to calculate 345 345 let date_ranges = vec![ 346 346 ("today", "Today"), 347 - ("this_week", "This Week"), 348 - ("this_month", "This Month"), 349 - ("next_week", "Next Week"), 350 - ("next_month", "Next Month"), 347 + ("this-week", "This Week"), 348 + ("this-month", "This Month"), 349 + ("next-week", "Next Week"), 350 + ("next-month", "Next Month"), 351 351 ]; 352 352 353 353 for (key, _label) in date_ranges { ··· 376 376 // Define date ranges to calculate 377 377 let date_ranges = vec![ 378 378 ("today", "Today"), 379 - ("this_week", "This Week"), 380 - ("this_month", "This Month"), 381 - ("next_week", "Next Week"), 382 - ("next_month", "Next Month"), 379 + ("this-week", "This Week"), 380 + ("this-month", "This Month"), 381 + ("next-week", "Next Week"), 382 + ("next-month", "Next Month"), 383 383 ]; 384 384 385 385 for (key, _label) in date_ranges { ··· 412 412 let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc(); 413 413 (start, end) 414 414 } 415 - "this_week" => { 415 + "this_week" | "this-week" => { 416 416 let days_since_monday = now.weekday().num_days_from_monday(); 417 417 let start = (now - chrono::Duration::days(days_since_monday as i64)) 418 418 .date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc(); 419 419 let end = start + chrono::Duration::days(6); 420 420 (start, end) 421 421 } 422 - "this_month" => { 422 + "this_month" | "this-month" => { 423 423 let start = now.date_naive().with_day(1).unwrap().and_hms_opt(0, 0, 0).unwrap().and_utc(); 424 424 let end = if now.month() == 12 { 425 425 chrono::NaiveDate::from_ymd_opt(now.year() + 1, 1, 1).unwrap() ··· 428 428 }.and_hms_opt(0, 0, 0).unwrap().and_utc(); 429 429 (start, end) 430 430 } 431 - "next_week" => { 431 + "next_week" | "next-week" => { 432 432 let days_until_next_monday = 7 - now.weekday().num_days_from_monday(); 433 433 let start = (now + chrono::Duration::days(days_until_next_monday as i64)) 434 434 .date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc(); 435 435 let end = start + chrono::Duration::days(6); 436 436 (start, end) 437 437 } 438 - "next_month" => { 438 + "next_month" | "next-month" => { 439 439 let start = if now.month() == 12 { 440 440 chrono::NaiveDate::from_ymd_opt(now.year() + 1, 1, 1).unwrap() 441 441 } else { ··· 588 588 /// Generate i18n key for mode facets 589 589 pub fn generate_mode_i18n_key(mode: &str) -> String { 590 590 match mode { 591 - "community.lexicon.calendar.event#inperson" => "mode-inperson".to_string(), 591 + "community.lexicon.calendar.event#inperson" => "mode-in-person".to_string(), 592 592 "community.lexicon.calendar.event#virtual" => "mode-virtual".to_string(), 593 593 "community.lexicon.calendar.event#hybrid" => "mode-hybrid".to_string(), 594 594 _ => format!("mode-{}", mode.to_lowercase().replace('#', "-").replace('.', "-")), ··· 607 607 } 608 608 } 609 609 610 - /// Get display name for mode facet value (locale-aware) 611 - #[allow(dead_code)] 612 - fn mode_display_name(mode: &str, locale: &LanguageIdentifier) -> String { 613 - let lang_str = locale.language.as_str(); 614 - match mode { 615 - "community.lexicon.calendar.event#inperson" => match lang_str { 616 - "en" => "In-Person".to_string(), 617 - "fr" => "En personne".to_string(), 618 - _ => "In-Person".to_string(), 619 - }, 620 - "community.lexicon.calendar.event#virtual" => match lang_str { 621 - "en" => "Virtual".to_string(), 622 - "fr" => "Virtuel".to_string(), 623 - _ => "Virtual".to_string(), 624 - }, 625 - "community.lexicon.calendar.event#hybrid" => match lang_str { 626 - "en" => "Hybrid".to_string(), 627 - "fr" => "Hybride".to_string(), 628 - _ => "Hybrid".to_string(), 629 - }, 630 - _ => mode.to_string(), 631 - } 632 - } 610 + 633 611 634 - /// Get display name for status facet value (locale-aware) 635 - #[allow(dead_code)] 636 - fn status_display_name(status: &str, locale: &LanguageIdentifier) -> String { 637 - let lang_str = locale.language.as_str(); 638 - match status { 639 - "community.lexicon.calendar.event#scheduled" => match lang_str { 640 - "en" => "Scheduled".to_string(), 641 - "fr" => "Planifié".to_string(), 642 - _ => "Scheduled".to_string(), 643 - }, 644 - "community.lexicon.calendar.event#rescheduled" => match lang_str { 645 - "en" => "Rescheduled".to_string(), 646 - "fr" => "Reporté".to_string(), 647 - _ => "Rescheduled".to_string(), 648 - }, 649 - "community.lexicon.calendar.event#cancelled" => match lang_str { 650 - "en" => "Cancelled".to_string(), 651 - "fr" => "Annulé".to_string(), 652 - _ => "Cancelled".to_string(), 653 - }, 654 - "community.lexicon.calendar.event#postponed" => match lang_str { 655 - "en" => "Postponed".to_string(), 656 - "fr" => "Reporté".to_string(), 657 - _ => "Postponed".to_string(), 658 - }, 659 - "community.lexicon.calendar.event#planned" => match lang_str { 660 - "en" => "Planned".to_string(), 661 - "fr" => "Planifié".to_string(), 662 - _ => "Planned".to_string(), 663 - }, 664 - _ => status.to_string(), 665 - } 666 - } 612 + 667 613 668 - /// Get display name for date range facet (locale-aware) 669 - #[allow(dead_code)] 670 - fn date_range_display_name(range_key: &str, locale: &LanguageIdentifier) -> String { 671 - let lang_str = locale.language.as_str(); 672 - match range_key { 673 - "today" => match lang_str { 674 - "en" => "Today".to_string(), 675 - "fr" => "Aujourd'hui".to_string(), 676 - _ => "Today".to_string(), 677 - }, 678 - "this_week" => match lang_str { 679 - "en" => "This Week".to_string(), 680 - "fr" => "Cette semaine".to_string(), 681 - _ => "This Week".to_string(), 682 - }, 683 - "this_month" => match lang_str { 684 - "en" => "This Month".to_string(), 685 - "fr" => "Ce mois".to_string(), 686 - _ => "This Month".to_string(), 687 - }, 688 - "next_week" => match lang_str { 689 - "en" => "Next Week".to_string(), 690 - "fr" => "La semaine prochaine".to_string(), 691 - _ => "Next Week".to_string(), 692 - }, 693 - "next_month" => match lang_str { 694 - "en" => "Next Month".to_string(), 695 - "fr" => "Le mois prochain".to_string(), 696 - _ => "Next Month".to_string(), 697 - }, 698 - _ => range_key.to_string(), 699 - } 700 - } 614 + 701 615 702 616 /// Get translated facet name with fallback to formatted readable name 703 617 pub fn get_translated_facet_name(&self, i18n_key: &str, locale: &LanguageIdentifier) -> String { ··· 749 663 fn test_generate_mode_i18n_key() { 750 664 assert_eq!( 751 665 FacetCalculator::generate_mode_i18n_key("community.lexicon.calendar.event#inperson"), 752 - "mode-inperson" 666 + "mode-in-person" 753 667 ); 754 668 755 669 assert_eq!(
+25 -3
src/filtering/hydration.rs
··· 199 199 format!("Failed to parse AT-URI: {}", event.aturi) 200 200 ))?; 201 201 202 - // Get organizer display name (fallback to DID prefix if not available) 203 - let organizer_display_name = format!("did:{}...", &event.did[4..12]); 202 + // Get organizer handle for URL generation 203 + let organizer_handle = handle_for_did(&self.pool, &event.did).await 204 + .map(|h| h.handle) 205 + .unwrap_or_else(|_| event.did.clone()); 206 + 207 + // Generate the correct URL format: /{handle_slug}/{event_rkey} 208 + let handle_slug = crate::http::utils::slug_from_handle(&organizer_handle); 209 + let rkey = parsed_uri.2; // The rkey is the third component of the parsed AT-URI 210 + let site_url = format!("/{}/{}", handle_slug, rkey); 211 + 212 + // Get organizer display name using the same logic as in handle_filter_events.rs 213 + let organizer_display_name = { 214 + // Only use the handle if it looks like a proper handle (contains a dot and doesn't start with "did:") 215 + if organizer_handle.contains('.') && !organizer_handle.starts_with("did:") { 216 + organizer_handle.clone() 217 + } else { 218 + // Fallback to a shortened DID if no proper handle is available 219 + if event.did.len() > 20 { 220 + format!("{}...", &event.did[..20]) 221 + } else { 222 + event.did.clone() 223 + } 224 + } 225 + }; 204 226 205 227 let event_view = EventView { 206 - site_url: "https://smokesignal.events".to_string(), 228 + site_url, 207 229 aturi: event.aturi.clone(), 208 230 cid: event.cid.clone(), 209 231 repository: parsed_uri.0,
+2 -2
src/filtering/i18n_integration_test.rs
··· 12 12 fn test_i18n_translation_functions() { 13 13 // Test mode key generation 14 14 let mode_key = FacetCalculator::generate_mode_i18n_key("community.lexicon.calendar.event#inperson"); 15 - assert_eq!(mode_key, "mode-inperson"); 15 + assert_eq!(mode_key, "mode-in-person"); 16 16 17 17 println!("Mode i18n key generation test:"); 18 18 println!(" inperson -> {}", mode_key); ··· 49 49 // Test the i18n key generation functions 50 50 assert_eq!( 51 51 FacetCalculator::generate_mode_i18n_key("community.lexicon.calendar.event#inperson"), 52 - "mode-inperson" 52 + "mode-in-person" 53 53 ); 54 54 55 55 assert_eq!(
+28 -4
src/http/handle_filter_events.rs
··· 346 346 }), 347 347 348 348 organizer_did: event.did.clone(), 349 - organizer_display_name: hydrated.creator_handle 350 - .as_ref() 351 - .map(|h| h.handle.clone()) 352 - .unwrap_or_else(|| event.did.clone()), 349 + organizer_display_name: { 350 + let display_name = hydrated.creator_handle 351 + .as_ref() 352 + .and_then(|h| { 353 + // Log what we received for debugging 354 + tracing::debug!("Handle for DID {}: '{}'", event.did, h.handle); 355 + 356 + // Only use the handle if it looks like a proper handle (contains a dot and doesn't start with "did:") 357 + if h.handle.contains('.') && !h.handle.starts_with("did:") { 358 + Some(h.handle.clone()) 359 + } else { 360 + tracing::debug!("Rejecting handle '{}' as it doesn't look like a proper handle", h.handle); 361 + None 362 + } 363 + }) 364 + .unwrap_or_else(|| { 365 + // Fallback to a shortened DID if no proper handle is available 366 + tracing::debug!("Using fallback display name for DID: {}", event.did); 367 + if event.did.len() > 20 { 368 + format!("{}...", &event.did[..20]) 369 + } else { 370 + event.did.clone() 371 + } 372 + }); 373 + 374 + tracing::debug!("Final organizer_display_name for DID {}: '{}'", event.did, display_name); 375 + display_name 376 + }, 353 377 354 378 starts_at_machine: event_details.starts_at.as_ref().map(|dt| dt.to_rfc3339()), 355 379 starts_at_human: event_details.starts_at.as_ref().map(|dt| {
+34
src/http/utils.rs
··· 156 156 } 157 157 ret.to_string() 158 158 } 159 + 160 + /// Convert a handle to a URL-safe slug format 161 + /// 162 + /// This function takes a handle (which may be in various formats like `example.com`, 163 + /// `@example.com`, `did:web:example.com`, or `did:plc:abc123`) and converts it to 164 + /// a URL-safe slug that can be used in URL paths. 165 + /// 166 + /// # Arguments 167 + /// * `handle` - The handle to convert to a slug 168 + /// 169 + /// # Returns 170 + /// * A URL-safe slug string 171 + pub fn slug_from_handle(handle: &str) -> String { 172 + // Strip common prefixes to get the core handle 173 + let trimmed = if let Some(value) = handle.strip_prefix("at://") { 174 + value 175 + } else if let Some(value) = handle.strip_prefix('@') { 176 + value 177 + } else { 178 + handle 179 + }; 180 + 181 + // For DID formats, we need to handle them specially 182 + if let Some(web_handle) = trimmed.strip_prefix("did:web:") { 183 + // For did:web: format, use the domain part 184 + web_handle.to_string() 185 + } else if trimmed.starts_with("did:plc:") { 186 + // For did:plc: format, use the full DID as the slug 187 + trimmed.to_string() 188 + } else { 189 + // For regular handles, use as-is (they're already domain-like) 190 + trimmed.to_string() 191 + } 192 + }