From 12a7d9e79aff46f864cf63061f0b1429115935bf Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Sun, 10 Nov 2024 22:02:42 +0100 Subject: [PATCH 01/13] This is should be illegal --- leptos_i18n/src/lib.rs | 5 +- leptos_i18n/src/locale_traits.rs | 2 + leptos_i18n/src/macros.rs | 12 + leptos_i18n/src/routing.rs | 266 ++++++++++++++++++++-- leptos_i18n/src/scopes.rs | 5 +- leptos_i18n_macro/src/load_locales/mod.rs | 9 +- 6 files changed, 278 insertions(+), 21 deletions(-) diff --git a/leptos_i18n/src/lib.rs b/leptos_i18n/src/lib.rs index 902f16de..96ca52b1 100644 --- a/leptos_i18n/src/lib.rs +++ b/leptos_i18n/src/lib.rs @@ -168,7 +168,10 @@ pub mod __private { #[cfg(feature = "plurals")] pub use crate::formatting::get_plural_rules; pub use crate::macro_helpers::*; - pub use crate::routing::{i18n_routing, BaseRoute, I18nNestedRoute}; + pub use crate::routing::{ + i18n_routing, make_i18n_segment, BaseRoute, I18nNestedRoute, I18nSegment, + InnerRouteSegments, + }; pub use leptos_i18n_macro as macros_reexport; } diff --git a/leptos_i18n/src/locale_traits.rs b/leptos_i18n/src/locale_traits.rs index 8233fbb8..cfbb06b6 100644 --- a/leptos_i18n/src/locale_traits.rs +++ b/leptos_i18n/src/locale_traits.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use std::{fmt::Debug, hash::Hash}; use crate::langid::{convert_vec_str_to_langids_lossy, filter_matches, find_match}; +use crate::routing::InnerRouteSegments; /// Trait implemented the enum representing the supported locales of the application /// @@ -95,6 +96,7 @@ pub trait Locale: fn make_routes( base_route: crate::routing::BaseRoute, base_path: &'static str, + segments: InnerRouteSegments, ) -> Self::Routes where View: ChooseView; diff --git a/leptos_i18n/src/macros.rs b/leptos_i18n/src/macros.rs index 9b1d9617..1a0dab97 100644 --- a/leptos_i18n/src/macros.rs +++ b/leptos_i18n/src/macros.rs @@ -948,3 +948,15 @@ macro_rules! tu_plural_ordinal { $crate::__private::macros_reexport::tu_plural_ordinal!{$($tt)*} }; } + +/// Create a route segment that is possible to define based on a locale. +/// +/// ```rust, ignore +/// +/// `` +#[macro_export] +macro_rules! i18n_path { + ($t:ty, $func:expr) => {{ + leptos_i18n::__private::make_i18n_segment::<$t, _>($func) + }}; +} diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index f72a8216..13df8569 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -1,4 +1,12 @@ -use std::{future::Future, sync::Arc}; +use std::{ + any::Any, + cell::RefCell, + collections::{HashMap, HashSet}, + fmt::Debug, + future::Future, + marker::PhantomData, + sync::{Arc, Mutex}, +}; use leptos::{either::Either, ev, prelude::*}; use leptos_router::{ @@ -44,33 +52,147 @@ impl<'a> PathBuilder<'a> { } } +fn match_path_segments(segments: &[&str], old_segments: &[PathSegment]) -> Option> { + // This hurt my eyes + + let mut optionals = HashSet::new(); + + let mut segments_iter = old_segments.iter().enumerate(); + 'outer: for seg in segments { + 'inner: loop { + let (index, next_seg) = segments_iter.next()?; + + match next_seg { + PathSegment::Unit => continue 'inner, + PathSegment::Param(_) => continue 'outer, + PathSegment::OptionalParam(to_match) if to_match == seg => { + optionals.insert(index); + continue 'outer; + } + PathSegment::OptionalParam(_) => continue 'inner, + PathSegment::Static(to_match) if to_match.is_empty() => continue 'inner, + PathSegment::Static(to_match) if to_match == seg => continue 'outer, + PathSegment::Static(_) => return None, + PathSegment::Splat(_) => return Some(optionals), + } + } + } + + // if iter is empty, perfect match ! + segments_iter.next().is_none().then_some(optionals) +} + +fn construct_path_segments<'b, 'p: 'b>( + segments: &[&'p str], + new_segments: &'p [PathSegment], + path_builder: &mut PathBuilder<'b>, + optionals: &HashSet, +) { + let mut segments_iter = new_segments.iter().enumerate(); + 'outer: for seg in segments { + 'inner: loop { + let (index, next_seg) = segments_iter.next().unwrap(); + + match next_seg { + PathSegment::Unit => continue 'inner, + PathSegment::Param(_) => { + path_builder.push(seg); + continue 'outer; + } + PathSegment::OptionalParam(_) if optionals.contains(&index) => { + path_builder.push(seg); + continue 'outer; + } + PathSegment::OptionalParam(_) => continue 'inner, + PathSegment::Static(to_push) if to_push.is_empty() => continue 'inner, + PathSegment::Static(to_push) => { + path_builder.push(to_push); + continue 'outer; + } + PathSegment::Splat(_) => { + todo!() + } + } + } + } +} + +fn localize_path<'b, 'p: 'b>( + path: &'p str, + old_locale_segments: &[Vec], + new_locale_segments: &'p [Vec], + path_builder: &mut PathBuilder<'b>, +) -> Option<()> { + let path_segments = path + .split('/') + .filter(|s| !s.is_empty()) + .collect::>(); + + let (pos, optionals) = + old_locale_segments + .iter() + .enumerate() + .find_map(|(pos, old_segments)| { + match_path_segments(&path_segments, old_segments).map(|op| (pos, op)) + })?; + + let new_segments = &new_locale_segments[pos]; + + construct_path_segments(&path_segments, new_segments, path_builder, &optionals); + + Some(()) +} + fn get_new_path( location: &Location, base_path: &str, new_locale: L, locale: Option, + segments: InnerRouteSegments, ) -> String { + let _ = segments; let mut new_path = location.pathname.with_untracked(|path_name| { + let segments = segments.lock().unwrap(); let mut path_builder = PathBuilder::default(); path_builder.push(base_path); if new_locale != L::default() { path_builder.push(new_locale.as_str()); } if let Some(path_rest) = path_name.strip_prefix(base_path) { - match locale { - None => path_builder.push(path_rest), + let path_rest = match locale { + None => path_rest, Some(l) => { if let Some(path_rest) = path_rest.strip_prefix(l.as_str()) { - path_builder.push(path_rest) + path_rest } else { - path_builder.push(path_rest) // Should happen only if l == L::default() + path_rest // Should happen only if l == L::default() } } + }; + + let old_locale_segments = segments.get(&locale.unwrap_or_default()); + let new_locale_segments = segments.get(&new_locale); + + let localized = match (old_locale_segments, new_locale_segments) { + (Some(old_locale_segments), Some(new_locale_segments)) => localize_path( + path_rest, + old_locale_segments, + new_locale_segments, + &mut path_builder, + ) + .is_some(), + _ => false, + }; + + if !localized { + path_builder.push(path_rest); } + // else ? } path_builder.build() }); + location.search.with_untracked(|search| { if !search.is_empty() { new_path.push('?'); @@ -91,6 +213,7 @@ fn update_path_effect( i18n: I18nContext, base_path: &'static str, history_changed_locale: StoredValue>, + segments: InnerRouteSegments, ) -> impl Fn(Option) -> L + 'static { let location = use_location(); let navigate = use_navigate(); @@ -108,7 +231,13 @@ fn update_path_effect( return new_locale; } - let new_path = get_new_path(&location, base_path, new_locale, Some(prev_loc)); + let new_path = get_new_path( + &location, + base_path, + new_locale, + Some(prev_loc), + segments.clone(), + ); let navigate = navigate.clone(); @@ -132,6 +261,7 @@ fn update_path_effect( fn correct_locale_prefix_effect( i18n: I18nContext, base_path: &'static str, + segments: InnerRouteSegments, ) -> impl Fn(Option<()>) + 'static { let location = use_location(); let navigate = use_navigate(); @@ -143,7 +273,13 @@ fn correct_locale_prefix_effect( return; } - let new_path = get_new_path(&location, base_path, current_locale, path_locale); + let new_path = get_new_path( + &location, + base_path, + current_locale, + path_locale, + segments.clone(), + ); let navigate = navigate.clone(); @@ -191,12 +327,22 @@ fn check_history_change( } } -fn maybe_redirect(previously_resolved_locale: L, base_path: &str) -> Option { +fn maybe_redirect( + previously_resolved_locale: L, + base_path: &str, + segments: InnerRouteSegments, +) -> Option { let location = use_location(); if cfg!(not(feature = "ssr")) || previously_resolved_locale == L::default() { return None; } - let new_path = get_new_path(&location, base_path, previously_resolved_locale, None); + let new_path = get_new_path( + &location, + base_path, + previously_resolved_locale, + None, + segments, + ); Some(new_path) } @@ -233,6 +379,7 @@ fn view_wrapper( view: View, route_locale: Option, base_path: &'static str, + segments: InnerRouteSegments, ) -> Either { let i18n = use_i18n_context::(); @@ -245,7 +392,7 @@ fn view_wrapper( i18n.set_locale(locale); None } else { - maybe_redirect(previously_resolved_locale, base_path) + maybe_redirect(previously_resolved_locale, base_path, segments.clone()) }; // This variable is there to sync history changes, because we step out of the Leptos routes reactivity we don't get forward and backward history changes triggers @@ -255,7 +402,12 @@ fn view_wrapper( // it starts at None such that on the first render the effect don't change the locale instantly. let history_changed_locale = StoredValue::new(None); - Effect::new(update_path_effect(i18n, base_path, history_changed_locale)); + Effect::new(update_path_effect( + i18n, + base_path, + history_changed_locale, + segments.clone(), + )); // listen for history changes let handle = window_event_listener( @@ -266,7 +418,7 @@ fn view_wrapper( on_cleanup(move || handle.remove()); // correct the url when using that removes the locale prefix - Effect::new(correct_locale_prefix_effect(i18n, base_path)); + Effect::new(correct_locale_prefix_effect(i18n, base_path, segments)); match redir { None => Either::Left(view), @@ -286,6 +438,7 @@ pub fn i18n_routing( ) -> L::Routes where View: ChooseView, + L::Routes: MatchNestedRoutes, { let children = children.into_inner(); let base_route = NestedRoute::new(StaticSegment(""), view) @@ -293,14 +446,24 @@ where .child(children); let base_route = Arc::new(base_route); - L::make_routes(base_route, base_path) + let segments = InnerRouteSegments::::default(); + + let routes = L::make_routes(base_route, base_path, segments); + + routes.generate_routes().into_iter().count(); + + routes } +#[doc(hidden)] +pub type InnerRouteSegments = Arc>>>>; + #[doc(hidden)] pub struct I18nNestedRoute { route: Arc, Chil, (), View>>, locale: Option, base_path: &'static str, + segments: InnerRouteSegments, } impl Clone for I18nNestedRoute { @@ -308,10 +471,12 @@ impl Clone for I18nNestedRoute { let route = self.route.clone(); let locale = self.locale.clone(); let base_path = self.base_path; + let segments = self.segments.clone(); I18nNestedRoute { route, locale, base_path, + segments, } } } @@ -321,11 +486,13 @@ impl I18nNestedRoute { locale: Option, base_path: &'static str, route: Arc, Chil, (), View>>, + segments: InnerRouteSegments, ) -> Self { Self { route, locale, base_path, + segments, } } } @@ -342,6 +509,10 @@ impl I18nNestedRoute { #[doc(hidden)] pub type BaseRoute = Arc, Chil, (), View>>; +thread_local! { + static CURRENT_ROUTE_LOCALE: RefCell>> = const { RefCell::new(None) }; +} + #[doc(hidden)] pub struct I18nRouteMatch where @@ -354,6 +525,7 @@ where matched: String, inner_match: , Chil, (), View> as MatchNestedRoutes>::Match, + segments: InnerRouteSegments, } impl MatchParams for I18nRouteMatch @@ -386,7 +558,14 @@ where fn into_view_and_child(self) -> (impl ChooseView, Option) { let (view, child) = MatchInterface::into_view_and_child(self.inner_match); - let new_view = Arc::new(move || view_wrapper(view.clone(), self.locale, self.base_path)); + let new_view = Arc::new(move || { + view_wrapper( + view.clone(), + self.locale, + self.base_path, + self.segments.clone(), + ) + }); (ViewWrapper(new_view), child) } } @@ -420,6 +599,7 @@ where matched, inner_match, base_path: self.base_path, + segments: self.segments.clone(), }; Some((Some((route_match_id, route_match)), remaining)) }) @@ -433,6 +613,7 @@ where matched: String::new(), inner_match, base_path: self.base_path, + segments: self.segments.clone(), }; (Some((route_match_id, route_match)), remaining) }) @@ -441,9 +622,18 @@ where } fn generate_routes(&self) -> impl IntoIterator + '_ { + CURRENT_ROUTE_LOCALE.with_borrow_mut(|current_locale| { + *current_locale = Some(Box::new(self.locale.unwrap_or_default())) + }); + MatchNestedRoutes::generate_routes(&*self.route) .into_iter() .map(|mut generated_route| { + if let Some(locale) = self.locale { + let segments = generated_route.segments.clone(); + let mut guard = self.segments.lock().unwrap(); + guard.entry(locale).or_default().push(segments); + } if let (Some(locale), Some(first)) = (self.locale, generated_route.segments.first_mut()) { @@ -454,3 +644,51 @@ where }) } } + +#[doc(hidden)] +pub struct I18nSegment { + func: F, + marker: PhantomData, +} + +impl Debug for I18nSegment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("I18nSegment").finish() + } +} + +impl PossibleRouteMatch for I18nSegment +where + F: Fn(L) -> &'static str, +{ + fn test<'a>(&self, path: &'a str) -> Option> { + let locale = leptos::prelude::use_context::>() + .map(I18nContext::get_locale_untracked) + .unwrap_or_default(); + let seg = (self.func)(locale); + StaticSegment(seg).test(path) + } + + fn generate_path(&self, path: &mut Vec) { + let locale = CURRENT_ROUTE_LOCALE.with_borrow(|locale| { + locale + .as_ref() + .and_then(|l| l.downcast_ref::().copied()) + .unwrap_or_default() + }); + let seg = (self.func)(locale); + StaticSegment(seg).generate_path(path); + } +} + +#[doc(hidden)] +pub fn make_i18n_segment(f: F) -> I18nSegment +where + L: Locale, + F: Fn(L) -> &'static str + 'static + Send + Sync, +{ + I18nSegment { + func: f, + marker: PhantomData, + } +} diff --git a/leptos_i18n/src/scopes.rs b/leptos_i18n/src/scopes.rs index 51756872..842b155a 100644 --- a/leptos_i18n/src/scopes.rs +++ b/leptos_i18n/src/scopes.rs @@ -8,7 +8,7 @@ use std::{ use icu_locid::{LanguageIdentifier, Locale as IcuLocale}; -use crate::{Direction, I18nContext, Locale, LocaleKeys}; +use crate::{routing::InnerRouteSegments, Direction, I18nContext, Locale, LocaleKeys}; /// Represent a scope in a locale. pub trait Scope: 'static + Send + Sync { @@ -190,11 +190,12 @@ impl> Locale for ScopedLocale { fn make_routes( base_route: crate::routing::BaseRoute, base_path: &'static str, + segments: InnerRouteSegments, ) -> Self::Routes where View: ChooseView, { - L::make_routes(base_route, base_path) + L::make_routes(base_route, base_path, segments) } #[cfg(feature = "dynamic_load")] diff --git a/leptos_i18n_macro/src/load_locales/mod.rs b/leptos_i18n_macro/src/load_locales/mod.rs index 1a03745c..e8534c2f 100644 --- a/leptos_i18n_macro/src/load_locales/mod.rs +++ b/leptos_i18n_macro/src/load_locales/mod.rs @@ -312,7 +312,7 @@ fn load_locales_inner( /// `children` may be empty or include nested routes. children: RouteChildren, ) -> <#enum_ident as l_i18n_crate::Locale>::Routes - where View: ChooseView, + where View: ChooseView + 'static + Send + Sync, Chil: MatchNestedRoutes + 'static, { l_i18n_crate::__private::i18n_routing::<#enum_ident, View, Chil>(base_path, children, ssr, view) } @@ -379,9 +379,9 @@ fn create_locales_enum( let routes = fit_in_leptos_tuple(&routes); let make_routes = locales.iter().map(|locale| { - quote!(l_i18n_crate::__private::I18nNestedRoute::new(Some(Self::#locale), base_path, core::clone::Clone::clone(&base_route))) + quote!(l_i18n_crate::__private::I18nNestedRoute::new(Some(Self::#locale), base_path, core::clone::Clone::clone(&base_route), core::clone::Clone::clone(&segments))) }) - .chain(Some(quote!(l_i18n_crate::__private::I18nNestedRoute::new(None, base_path, base_route)))) + .chain(Some(quote!(l_i18n_crate::__private::I18nNestedRoute::new(None, base_path, base_route, segments)))) .collect::>(); let make_routes = fit_in_leptos_tuple(&make_routes); @@ -532,7 +532,8 @@ fn create_locales_enum( fn make_routes( base_route: l_i18n_crate::__private::BaseRoute, - base_path: &'static str + base_path: &'static str, + segments: l_i18n_crate::__private::InnerRouteSegments ) -> Self::Routes where View: l_i18n_crate::reexports::leptos_router::ChooseView { From 2b33a0ee54b0d8e4d139cc4eb9673c25c4b6af77 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Sun, 10 Nov 2024 22:50:33 +0100 Subject: [PATCH 02/13] fix path matching --- leptos_i18n/src/routing.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 13df8569..20aaf906 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -585,6 +585,9 @@ where path: &'a str, ) -> (Option<(leptos_router::RouteMatchId, Self::Match)>, &'a str) { if let Some(locale) = self.locale { + CURRENT_ROUTE_LOCALE.with_borrow_mut(|loc| { + *loc = Some(Box::new(locale)); + }); StaticSegment(locale.as_str()) .test(path) .and_then(|partial_path_match| { @@ -605,6 +608,9 @@ where }) .unwrap_or((None, path)) } else { + CURRENT_ROUTE_LOCALE.with_borrow_mut(|loc| { + *loc = Some(Box::new(L::default())); + }); let (inner_match, remaining) = MatchNestedRoutes::match_nested(&*self.route, path); inner_match .map(|(route_match_id, inner_match)| { @@ -657,25 +663,27 @@ impl Debug for I18nSegment { } } +fn get_current_route_locale() -> L { + CURRENT_ROUTE_LOCALE.with_borrow(|locale| { + locale + .as_ref() + .and_then(|l| l.downcast_ref::().copied()) + .unwrap_or_default() + }) +} + impl PossibleRouteMatch for I18nSegment where F: Fn(L) -> &'static str, { fn test<'a>(&self, path: &'a str) -> Option> { - let locale = leptos::prelude::use_context::>() - .map(I18nContext::get_locale_untracked) - .unwrap_or_default(); + let locale = get_current_route_locale(); let seg = (self.func)(locale); StaticSegment(seg).test(path) } fn generate_path(&self, path: &mut Vec) { - let locale = CURRENT_ROUTE_LOCALE.with_borrow(|locale| { - locale - .as_ref() - .and_then(|l| l.downcast_ref::().copied()) - .unwrap_or_default() - }); + let locale = get_current_route_locale(); let seg = (self.func)(locale); StaticSegment(seg).generate_path(path); } From 668b1e45aee27e37e4128f72db2a3c8a0d10deab Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Sun, 10 Nov 2024 23:09:11 +0100 Subject: [PATCH 03/13] prevent to populate the segment map multiple times --- leptos_i18n/src/routing.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 20aaf906..82830506 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -152,7 +152,7 @@ fn get_new_path( ) -> String { let _ = segments; let mut new_path = location.pathname.with_untracked(|path_name| { - let segments = segments.lock().unwrap(); + let segments = segments.0.lock().unwrap(); let mut path_builder = PathBuilder::default(); path_builder.push(base_path); if new_locale != L::default() { @@ -170,8 +170,8 @@ fn get_new_path( } }; - let old_locale_segments = segments.get(&locale.unwrap_or_default()); - let new_locale_segments = segments.get(&new_locale); + let old_locale_segments = segments.0.get(&locale.unwrap_or_default()); + let new_locale_segments = segments.0.get(&new_locale); let localized = match (old_locale_segments, new_locale_segments) { (Some(old_locale_segments), Some(new_locale_segments)) => localize_path( @@ -448,15 +448,21 @@ where let segments = InnerRouteSegments::::default(); - let routes = L::make_routes(base_route, base_path, segments); + let routes = L::make_routes(base_route, base_path, segments.clone()); routes.generate_routes().into_iter().count(); + let mut guard = segments.0.lock().unwrap(); + + guard.1 = true; + routes } #[doc(hidden)] -pub type InnerRouteSegments = Arc>>>>; +#[derive(Clone, Default)] +#[allow(clippy::type_complexity)] +pub struct InnerRouteSegments(Arc>>, bool)>>); #[doc(hidden)] pub struct I18nNestedRoute { @@ -636,9 +642,11 @@ where .into_iter() .map(|mut generated_route| { if let Some(locale) = self.locale { - let segments = generated_route.segments.clone(); - let mut guard = self.segments.lock().unwrap(); - guard.entry(locale).or_default().push(segments); + let mut guard = self.segments.0.lock().unwrap(); + if !guard.1 { + let segments = generated_route.segments.clone(); + guard.0.entry(locale).or_default().push(segments); + } } if let (Some(locale), Some(first)) = (self.locale, generated_route.segments.first_mut()) From 76fb99451d8c71161e14c8f112d5a543f0cde11a Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 11 Nov 2024 17:49:58 +0100 Subject: [PATCH 04/13] fixed bad navigatin when using anchors to set locale --- leptos_i18n/src/routing.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 82830506..cd933af4 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -218,6 +218,7 @@ fn update_path_effect( let location = use_location(); let navigate = use_navigate(); move |prev_loc: Option| { + let path_locale = get_locale_from_path::(&location, base_path); let new_locale = i18n.get_locale(); // don't react on history change. if let Some(new_locale) = history_changed_locale.get_value() { @@ -227,7 +228,7 @@ fn update_path_effect( let Some(prev_loc) = prev_loc else { return new_locale; }; - if new_locale == prev_loc { + if new_locale == prev_loc || new_locale == path_locale.unwrap_or_default() { return new_locale; } @@ -273,16 +274,20 @@ fn correct_locale_prefix_effect( return; } + let new_locale = path_locale.unwrap_or(current_locale); + let new_path = get_new_path( &location, base_path, - current_locale, + new_locale, path_locale, segments.clone(), ); let navigate = navigate.clone(); + i18n.set_locale(new_locale); + // TODO FIXME: see https://github.com/leptos-rs/leptos/issues/2979 // It works for now, but it is not ideal. request_animation_frame(move || { From 6f9722401778335e1f7965a6fe85a868b633f076 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 11 Nov 2024 19:58:33 +0100 Subject: [PATCH 05/13] fixed history --- leptos_i18n/src/routing.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index cd933af4..032f8f79 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -263,6 +263,7 @@ fn correct_locale_prefix_effect( i18n: I18nContext, base_path: &'static str, segments: InnerRouteSegments, + history_changed: StoredValue, ) -> impl Fn(Option<()>) + 'static { let location = use_location(); let navigate = use_navigate(); @@ -274,7 +275,12 @@ fn correct_locale_prefix_effect( return; } - let new_locale = path_locale.unwrap_or(current_locale); + let new_locale = if history_changed.get_value() { + history_changed.set_value(false); + current_locale + } else { + path_locale.unwrap_or(current_locale) + }; let new_path = get_new_path( &location, @@ -318,6 +324,7 @@ fn check_history_change( i18n: I18nContext, base_path: &'static str, sync: StoredValue>, + history_changed: StoredValue, ) -> impl Fn(ev::PopStateEvent) + 'static { let location = use_location(); @@ -325,6 +332,7 @@ fn check_history_change( let path_locale = get_locale_from_path::(&location, base_path).unwrap_or_default(); sync.set_value(Some(path_locale)); + history_changed.set_value(true); if i18n.get_locale_untracked() != path_locale { i18n.set_locale(path_locale); @@ -405,25 +413,26 @@ fn view_wrapper( // but changing the locale on history change will trigger the locale change effect, causing to change the URL again but with a wrong previous locale // so this variable sync them together on what is the locale currently in the URL. // it starts at None such that on the first render the effect don't change the locale instantly. - let history_changed_locale = StoredValue::new(None); + let sync = StoredValue::new(None); + let history_changed = StoredValue::new(false); - Effect::new(update_path_effect( - i18n, - base_path, - history_changed_locale, - segments.clone(), - )); + Effect::new(update_path_effect(i18n, base_path, sync, segments.clone())); // listen for history changes let handle = window_event_listener( ev::popstate, - check_history_change(i18n, base_path, history_changed_locale), + check_history_change(i18n, base_path, sync, history_changed), ); on_cleanup(move || handle.remove()); // correct the url when using that removes the locale prefix - Effect::new(correct_locale_prefix_effect(i18n, base_path, segments)); + Effect::new(correct_locale_prefix_effect( + i18n, + base_path, + segments, + history_changed, + )); match redir { None => Either::Left(view), From de2a7caa9f38bdcc88b6f36cbc584e8bbca556fc Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 11 Nov 2024 22:34:46 +0100 Subject: [PATCH 06/13] router refactor --- leptos_i18n/src/lib.rs | 5 +- leptos_i18n/src/locale_traits.rs | 14 -- leptos_i18n/src/routing.rs | 243 ++++++++++++---------- leptos_i18n/src/scopes.rs | 15 +- leptos_i18n_macro/src/load_locales/mod.rs | 37 +--- 5 files changed, 133 insertions(+), 181 deletions(-) diff --git a/leptos_i18n/src/lib.rs b/leptos_i18n/src/lib.rs index 96ca52b1..d729c067 100644 --- a/leptos_i18n/src/lib.rs +++ b/leptos_i18n/src/lib.rs @@ -168,10 +168,7 @@ pub mod __private { #[cfg(feature = "plurals")] pub use crate::formatting::get_plural_rules; pub use crate::macro_helpers::*; - pub use crate::routing::{ - i18n_routing, make_i18n_segment, BaseRoute, I18nNestedRoute, I18nSegment, - InnerRouteSegments, - }; + pub use crate::routing::{i18n_routing, make_i18n_segment, BaseRoute, I18nSegment}; pub use leptos_i18n_macro as macros_reexport; } diff --git a/leptos_i18n/src/locale_traits.rs b/leptos_i18n/src/locale_traits.rs index cfbb06b6..c7494dd9 100644 --- a/leptos_i18n/src/locale_traits.rs +++ b/leptos_i18n/src/locale_traits.rs @@ -1,10 +1,8 @@ use icu_locid::{LanguageIdentifier, Locale as IcuLocale}; -use leptos_router::ChooseView; use std::str::FromStr; use std::{fmt::Debug, hash::Hash}; use crate::langid::{convert_vec_str_to_langids_lossy, filter_matches, find_match}; -use crate::routing::InnerRouteSegments; /// Trait implemented the enum representing the supported locales of the application /// @@ -32,9 +30,6 @@ pub trait Locale: /// The associated struct containing the translations type Keys: LocaleKeys; - /// Associated routes for routing - type Routes; - /// Associated `#[server]` function type to request the translations #[cfg(feature = "dynamic_load")] type ServerFn: leptos::server_fn::ServerFn; @@ -92,15 +87,6 @@ pub trait Locale: Self::from_base_locale(locale) } - /// Make the routes - fn make_routes( - base_route: crate::routing::BaseRoute, - base_path: &'static str, - segments: InnerRouteSegments, - ) -> Self::Routes - where - View: ChooseView; - /// Associated `#[server]` function to request the translations #[cfg(feature = "dynamic_load")] fn request_translations( diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 032f8f79..a221a9c4 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -1,5 +1,4 @@ use std::{ - any::Any, cell::RefCell, collections::{HashMap, HashSet}, fmt::Debug, @@ -148,7 +147,7 @@ fn get_new_path( base_path: &str, new_locale: L, locale: Option, - segments: InnerRouteSegments, + segments: RouteSegments, ) -> String { let _ = segments; let mut new_path = location.pathname.with_untracked(|path_name| { @@ -170,8 +169,8 @@ fn get_new_path( } }; - let old_locale_segments = segments.0.get(&locale.unwrap_or_default()); - let new_locale_segments = segments.0.get(&new_locale); + let old_locale_segments = segments.get(&locale.unwrap_or_default()); + let new_locale_segments = segments.get(&new_locale); let localized = match (old_locale_segments, new_locale_segments) { (Some(old_locale_segments), Some(new_locale_segments)) => localize_path( @@ -213,7 +212,7 @@ fn update_path_effect( i18n: I18nContext, base_path: &'static str, history_changed_locale: StoredValue>, - segments: InnerRouteSegments, + segments: RouteSegments, ) -> impl Fn(Option) -> L + 'static { let location = use_location(); let navigate = use_navigate(); @@ -262,7 +261,7 @@ fn update_path_effect( fn correct_locale_prefix_effect( i18n: I18nContext, base_path: &'static str, - segments: InnerRouteSegments, + segments: RouteSegments, history_changed: StoredValue, ) -> impl Fn(Option<()>) + 'static { let location = use_location(); @@ -343,7 +342,7 @@ fn check_history_change( fn maybe_redirect( previously_resolved_locale: L, base_path: &str, - segments: InnerRouteSegments, + segments: RouteSegments, ) -> Option { let location = use_location(); if cfg!(not(feature = "ssr")) || previously_resolved_locale == L::default() { @@ -392,7 +391,7 @@ fn view_wrapper( view: View, route_locale: Option, base_path: &'static str, - segments: InnerRouteSegments, + segments: RouteSegments, ) -> Either { let i18n = use_i18n_context::(); @@ -449,68 +448,49 @@ pub fn i18n_routing( children: RouteChildren, ssr_mode: SsrMode, view: View, -) -> L::Routes +) -> impl MatchNestedRoutes + Clone + Send + 'static where - View: ChooseView, - L::Routes: MatchNestedRoutes, + View: ChooseView + Clone + Send + Sync, + Chil: MatchNestedRoutes + 'static + Send + Sync + Clone, { let children = children.into_inner(); let base_route = NestedRoute::new(StaticSegment(""), view) .ssr_mode(ssr_mode) .child(children); - let base_route = Arc::new(base_route); - let segments = InnerRouteSegments::::default(); + let segments = RouteSegments::::default(); - let routes = L::make_routes(base_route, base_path, segments.clone()); + let routes = I18nNestedRoute::new(base_path, base_route, segments.clone()); - routes.generate_routes().into_iter().count(); + let inner_segments = routes.generate_routes_for_each_locale(); let mut guard = segments.0.lock().unwrap(); - guard.1 = true; + *guard = inner_segments; routes } -#[doc(hidden)] #[derive(Clone, Default)] -#[allow(clippy::type_complexity)] -pub struct InnerRouteSegments(Arc>>, bool)>>); +struct RouteSegments(Arc>>); -#[doc(hidden)] -pub struct I18nNestedRoute { - route: Arc, Chil, (), View>>, - locale: Option, - base_path: &'static str, - segments: InnerRouteSegments, -} +type RouteSegmentsInner = HashMap>>; -impl Clone for I18nNestedRoute { - fn clone(&self) -> Self { - let route = self.route.clone(); - let locale = self.locale.clone(); - let base_path = self.base_path; - let segments = self.segments.clone(); - I18nNestedRoute { - route, - locale, - base_path, - segments, - } - } +#[derive(Clone)] +struct I18nNestedRoute { + route: BaseRoute, + base_path: &'static str, + segments: RouteSegments, } -impl I18nNestedRoute { +impl I18nNestedRoute { pub fn new( - locale: Option, base_path: &'static str, - route: Arc, Chil, (), View>>, - segments: InnerRouteSegments, + route: BaseRoute, + segments: RouteSegments, ) -> Self { Self { route, - locale, base_path, segments, } @@ -527,10 +507,10 @@ impl I18nNestedRoute { // All the stupidity you will see under this comment is done just to archieve this. #[doc(hidden)] -pub type BaseRoute = Arc, Chil, (), View>>; +pub type BaseRoute = NestedRoute, Chil, (), View>; thread_local! { - static CURRENT_ROUTE_LOCALE: RefCell>> = const { RefCell::new(None) }; + static CURRENT_ROUTE_LOCALE: RefCell<&'static str> = const { RefCell::new("") }; } #[doc(hidden)] @@ -543,9 +523,8 @@ where locale: Option, base_path: &'static str, matched: String, - inner_match: - , Chil, (), View> as MatchNestedRoutes>::Match, - segments: InnerRouteSegments, + inner_match: as MatchNestedRoutes>::Match, + segments: RouteSegments, } impl MatchParams for I18nRouteMatch @@ -566,7 +545,7 @@ where Chil::Match: MatchParams, View: ChooseView + Clone + Sync, { - type Child = <, Chil, (), View> as MatchNestedRoutes>::Match as MatchInterface>::Child; + type Child = < as MatchNestedRoutes>::Match as MatchInterface>::Child; fn as_id(&self) -> leptos_router::RouteMatchId { MatchInterface::as_id(&self.inner_match) @@ -604,36 +583,34 @@ where &'a self, path: &'a str, ) -> (Option<(leptos_router::RouteMatchId, Self::Match)>, &'a str) { - if let Some(locale) = self.locale { - CURRENT_ROUTE_LOCALE.with_borrow_mut(|loc| { - *loc = Some(Box::new(locale)); - }); - StaticSegment(locale.as_str()) - .test(path) - .and_then(|partial_path_match| { - let remaining = partial_path_match.remaining(); - let matched = partial_path_match.matched(); - let (inner_match, remaining) = - MatchNestedRoutes::match_nested(&*self.route, remaining); - let (route_match_id, inner_match) = inner_match?; - let matched = matched.to_string(); - let route_match = I18nRouteMatch { - locale: Some(locale), - matched, - inner_match, - base_path: self.base_path, - segments: self.segments.clone(), - }; - Some((Some((route_match_id, route_match)), remaining)) - }) - .unwrap_or((None, path)) - } else { - CURRENT_ROUTE_LOCALE.with_borrow_mut(|loc| { - *loc = Some(Box::new(L::default())); - }); - let (inner_match, remaining) = MatchNestedRoutes::match_nested(&*self.route, path); - inner_match - .map(|(route_match_id, inner_match)| { + L::get_all() + .iter() + .copied() + .find_map(|locale| { + set_current_route_locale(locale); + StaticSegment(locale.as_str()) + .test(path) + .and_then(|partial_path_match| { + let remaining = partial_path_match.remaining(); + let matched = partial_path_match.matched(); + let (inner_match, remaining) = + MatchNestedRoutes::match_nested(&self.route, remaining); + let (route_match_id, inner_match) = inner_match?; + let matched = matched.to_string(); + let route_match = I18nRouteMatch { + locale: Some(locale), + matched, + inner_match, + base_path: self.base_path, + segments: self.segments.clone(), + }; + Some((Some((route_match_id, route_match)), remaining)) + }) + }) + .or_else(|| { + set_current_route_locale(L::default()); + let (inner_match, remaining) = MatchNestedRoutes::match_nested(&self.route, path); + inner_match.map(|(route_match_id, inner_match)| { let route_match = I18nRouteMatch { locale: None, matched: String::new(), @@ -643,33 +620,61 @@ where }; (Some((route_match_id, route_match)), remaining) }) - .unwrap_or((None, path)) - } + }) + .unwrap_or((None, path)) } fn generate_routes(&self) -> impl IntoIterator + '_ { - CURRENT_ROUTE_LOCALE.with_borrow_mut(|current_locale| { - *current_locale = Some(Box::new(self.locale.unwrap_or_default())) - }); - - MatchNestedRoutes::generate_routes(&*self.route) - .into_iter() - .map(|mut generated_route| { - if let Some(locale) = self.locale { - let mut guard = self.segments.0.lock().unwrap(); - if !guard.1 { - let segments = generated_route.segments.clone(); - guard.0.entry(locale).or_default().push(segments); - } - } - if let (Some(locale), Some(first)) = - (self.locale, generated_route.segments.first_mut()) - { - // replace the empty segment set by the inner route with the locale one - *first = PathSegment::Static(locale.as_str().into()) - } - generated_route + let default_locale_routes = std::iter::once_with(|| { + set_current_route_locale(L::default()); + MatchNestedRoutes::generate_routes(&self.route) + .into_iter() + .map(|mut generated_route| { + // remove empty segment set by the inner route + generated_route.segments.remove(0); + generated_route + }) + }) + .flatten(); + L::get_all() + .iter() + .copied() + .flat_map(|locale| { + set_current_route_locale(locale); + MatchNestedRoutes::generate_routes(&self.route) + .into_iter() + .map(move |mut generated_route| { + if let Some(first) = generated_route.segments.first_mut() { + // replace the empty segment set by the inner route with the locale one + *first = PathSegment::Static(locale.as_str().into()) + } + generated_route + }) }) + .chain(default_locale_routes) + } +} + +impl I18nNestedRoute +where + L: Locale, + View: Clone + Send + ChooseView, + Chil: MatchNestedRoutes + 'static, +{ + fn generate_routes_for_each_locale(&self) -> RouteSegmentsInner { + let mut segments = RouteSegmentsInner::default(); + + for locale in L::get_all() { + set_current_route_locale(*locale); + let inner_segments: Vec<_> = MatchNestedRoutes::generate_routes(&self.route) + .into_iter() + .map(|generated_route| generated_route.segments) + .collect(); + + segments.insert(*locale, inner_segments); + } + + segments } } @@ -685,29 +690,37 @@ impl Debug for I18nSegment { } } -fn get_current_route_locale() -> L { - CURRENT_ROUTE_LOCALE.with_borrow(|locale| { - locale - .as_ref() - .and_then(|l| l.downcast_ref::().copied()) - .unwrap_or_default() +fn set_current_route_locale(new_locale: L) { + CURRENT_ROUTE_LOCALE.with_borrow_mut(|locale| { + *locale = new_locale.as_str(); }) } -impl PossibleRouteMatch for I18nSegment +fn get_current_route_locale() -> L { + CURRENT_ROUTE_LOCALE.with_borrow(|locale| L::from_str(locale).unwrap_or_default()) +} + +impl I18nSegment where F: Fn(L) -> &'static str, { - fn test<'a>(&self, path: &'a str) -> Option> { + fn get_segment(&self) -> StaticSegment<&'static str> { let locale = get_current_route_locale(); let seg = (self.func)(locale); - StaticSegment(seg).test(path) + StaticSegment(seg) + } +} + +impl PossibleRouteMatch for I18nSegment +where + F: Fn(L) -> &'static str, +{ + fn test<'a>(&self, path: &'a str) -> Option> { + self.get_segment().test(path) } fn generate_path(&self, path: &mut Vec) { - let locale = get_current_route_locale(); - let seg = (self.func)(locale); - StaticSegment(seg).generate_path(path); + self.get_segment().generate_path(path); } } diff --git a/leptos_i18n/src/scopes.rs b/leptos_i18n/src/scopes.rs index 842b155a..55346fcf 100644 --- a/leptos_i18n/src/scopes.rs +++ b/leptos_i18n/src/scopes.rs @@ -1,4 +1,3 @@ -use leptos_router::ChooseView; use std::{ fmt::{self, Debug}, hash::Hash, @@ -8,7 +7,7 @@ use std::{ use icu_locid::{LanguageIdentifier, Locale as IcuLocale}; -use crate::{routing::InnerRouteSegments, Direction, I18nContext, Locale, LocaleKeys}; +use crate::{Direction, I18nContext, Locale, LocaleKeys}; /// Represent a scope in a locale. pub trait Scope: 'static + Send + Sync { @@ -155,7 +154,6 @@ impl> FromStr for ScopedLocale { impl> Locale for ScopedLocale { type Keys = S::Keys; - type Routes = L::Routes; type TranslationUnitId = L::TranslationUnitId; #[cfg(feature = "dynamic_load")] type ServerFn = L::ServerFn; @@ -187,17 +185,6 @@ impl> Locale for ScopedLocale { } } - fn make_routes( - base_route: crate::routing::BaseRoute, - base_path: &'static str, - segments: InnerRouteSegments, - ) -> Self::Routes - where - View: ChooseView, - { - L::make_routes(base_route, base_path, segments) - } - #[cfg(feature = "dynamic_load")] fn request_translations( self, diff --git a/leptos_i18n_macro/src/load_locales/mod.rs b/leptos_i18n_macro/src/load_locales/mod.rs index e8534c2f..115525af 100644 --- a/leptos_i18n_macro/src/load_locales/mod.rs +++ b/leptos_i18n_macro/src/load_locales/mod.rs @@ -1,18 +1,14 @@ use std::{collections::BTreeMap, ops::Not}; -// pub mod cfg_file; pub mod declare_locales; -// pub mod error; pub mod interpolate; pub mod locale; pub mod parsed_value; +pub mod plurals; pub mod ranges; pub mod tracking; pub mod warning; -pub mod plurals; - -use crate::utils::fit_in_leptos_tuple; use icu::locid::LanguageIdentifier; use interpolate::Interpolation; use leptos_i18n_parser::{ @@ -311,8 +307,8 @@ fn load_locales_inner( ssr: SsrMode, /// `children` may be empty or include nested routes. children: RouteChildren, - ) -> <#enum_ident as l_i18n_crate::Locale>::Routes - where View: ChooseView + 'static + Send + Sync, Chil: MatchNestedRoutes + 'static, + ) -> impl MatchNestedRoutes + 'static + Send + Sync + Clone + where View: ChooseView + 'static + Send + Sync, Chil: MatchNestedRoutes + 'static + Send + Sync + Clone, { l_i18n_crate::__private::i18n_routing::<#enum_ident, View, Chil>(base_path, children, ssr, view) } @@ -370,22 +366,6 @@ fn create_locales_enum( .map(|(variant, constant)| quote!(#enum_ident::#variant => #constant)) .collect::>(); - let routes = std::iter::repeat(quote!( - l_i18n_crate::__private::I18nNestedRoute - )) - .take(locales.len() + 1) - .collect::>(); - - let routes = fit_in_leptos_tuple(&routes); - - let make_routes = locales.iter().map(|locale| { - quote!(l_i18n_crate::__private::I18nNestedRoute::new(Some(Self::#locale), base_path, core::clone::Clone::clone(&base_route), core::clone::Clone::clone(&segments))) - }) - .chain(Some(quote!(l_i18n_crate::__private::I18nNestedRoute::new(None, base_path, base_route, segments)))) - .collect::>(); - - let make_routes = fit_in_leptos_tuple(&make_routes); - let server_fn_mod = if cfg!(feature = "dynamic_load") { quote! { mod server_fn { @@ -486,7 +466,6 @@ fn create_locales_enum( impl l_i18n_crate::Locale for #enum_ident { type Keys = #keys_ident; - type Routes = #routes; type TranslationUnitId = #translation_unit_enum_ident; #server_fn_type @@ -530,16 +509,6 @@ fn create_locales_enum( locale } - fn make_routes( - base_route: l_i18n_crate::__private::BaseRoute, - base_path: &'static str, - segments: l_i18n_crate::__private::InnerRouteSegments - ) -> Self::Routes - where View: l_i18n_crate::reexports::leptos_router::ChooseView - { - #make_routes - } - #request_translations #init_translations From 5e2025c5e83871b8a99c7262e424177bc154d8c2 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 11 Nov 2024 23:24:57 +0100 Subject: [PATCH 07/13] removed request_animation_frame --- leptos_i18n/src/routing.rs | 42 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index a221a9c4..92466897 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -241,18 +241,14 @@ fn update_path_effect( let navigate = navigate.clone(); - // TODO FIXME: see https://github.com/leptos-rs/leptos/issues/2979 - // It works for now, but it is not ideal. - request_animation_frame(move || { - navigate( - &new_path, - NavigateOptions { - resolve: false, - scroll: false, - ..Default::default() - }, - ); - }); + navigate( + &new_path, + NavigateOptions { + resolve: false, + scroll: false, + ..Default::default() + }, + ); new_locale } @@ -293,19 +289,15 @@ fn correct_locale_prefix_effect( i18n.set_locale(new_locale); - // TODO FIXME: see https://github.com/leptos-rs/leptos/issues/2979 - // It works for now, but it is not ideal. - request_animation_frame(move || { - navigate( - &new_path, - NavigateOptions { - resolve: false, - replace: true, - scroll: false, - ..Default::default() - }, - ); - }); + navigate( + &new_path, + NavigateOptions { + resolve: false, + replace: true, + scroll: false, + ..Default::default() + }, + ); } } From de11dfa3bead2f6467cec56e21f58f685e8ca21a Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Mon, 11 Nov 2024 23:44:58 +0100 Subject: [PATCH 08/13] Revert "removed request_animation_frame" This reverts commit 5e2025c5e83871b8a99c7262e424177bc154d8c2. --- leptos_i18n/src/routing.rs | 42 +++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 92466897..a221a9c4 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -241,14 +241,18 @@ fn update_path_effect( let navigate = navigate.clone(); - navigate( - &new_path, - NavigateOptions { - resolve: false, - scroll: false, - ..Default::default() - }, - ); + // TODO FIXME: see https://github.com/leptos-rs/leptos/issues/2979 + // It works for now, but it is not ideal. + request_animation_frame(move || { + navigate( + &new_path, + NavigateOptions { + resolve: false, + scroll: false, + ..Default::default() + }, + ); + }); new_locale } @@ -289,15 +293,19 @@ fn correct_locale_prefix_effect( i18n.set_locale(new_locale); - navigate( - &new_path, - NavigateOptions { - resolve: false, - replace: true, - scroll: false, - ..Default::default() - }, - ); + // TODO FIXME: see https://github.com/leptos-rs/leptos/issues/2979 + // It works for now, but it is not ideal. + request_animation_frame(move || { + navigate( + &new_path, + NavigateOptions { + resolve: false, + replace: true, + scroll: false, + ..Default::default() + }, + ); + }); } } From bd7eca2576e992372a8e322e42c3846c3bcfc5f3 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Tue, 12 Nov 2024 18:15:34 +0100 Subject: [PATCH 09/13] derive Clone and Copy for the I18nSegment --- leptos_i18n/src/routing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index a221a9c4..06a90eb6 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -679,6 +679,7 @@ where } #[doc(hidden)] +#[derive(Clone, Copy)] pub struct I18nSegment { func: F, marker: PhantomData, From d6f1aa86491dba5815d5c793208d72cf05373b1f Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 13 Nov 2024 19:32:35 +0100 Subject: [PATCH 10/13] Option to initialize the context from url path --- leptos_i18n/src/context.rs | 24 ++++++++++-- leptos_i18n/src/fetch_locale.rs | 45 +++++++++++++++++++---- leptos_i18n/src/macros.rs | 2 +- leptos_i18n/src/routing.rs | 32 ++++++++++------ leptos_i18n_macro/src/load_locales/mod.rs | 10 ++++- 5 files changed, 87 insertions(+), 26 deletions(-) diff --git a/leptos_i18n/src/context.rs b/leptos_i18n/src/context.rs index f7233d24..34dd0b40 100644 --- a/leptos_i18n/src/context.rs +++ b/leptos_i18n/src/context.rs @@ -166,6 +166,10 @@ where cookie_options: CookieOptions, /// Options to pass to `leptos_use::use_locales`. ssr_lang_header_getter: UseLocalesOptions, + /// Try to parse the locale from the URL pathname, expect the basepath. (default to `None`). + /// If `None` do nothing, if `Some(base_path)` strip the URL from `base_path` then expect to found a path segment that represent a locale. + /// This is usefull when using the `I18nRoute` with usage of the context outside the router. + parse_locale_from_path: Option>, } impl Default for I18nContextOptions<'_, L> { @@ -175,6 +179,7 @@ impl Default for I18nContextOptions<'_, L> { cookie_name: Cow::Borrowed(COOKIE_PREFERED_LANG), cookie_options: Default::default(), ssr_lang_header_getter: Default::default(), + parse_locale_from_path: None, } } } @@ -187,6 +192,7 @@ pub fn init_i18n_context_with_options(options: I18nContextOptions) cookie_name, cookie_options, ssr_lang_header_getter, + parse_locale_from_path, } = options; let (lang_cookie, set_lang_cookie) = if ENABLE_COOKIE && enable_cookie { leptos_use::use_cookie_with_options::(&cookie_name, cookie_options) @@ -195,8 +201,11 @@ pub fn init_i18n_context_with_options(options: I18nContextOptions) (lang_cookie.into(), set_lang_cookie) }; - let initial_locale = - fetch_locale::fetch_locale(lang_cookie.get_untracked(), ssr_lang_header_getter); + let initial_locale = fetch_locale::fetch_locale( + lang_cookie.get_untracked(), + ssr_lang_header_getter, + parse_locale_from_path, + ); init_context_inner::(set_lang_cookie, initial_locale) } @@ -268,7 +277,7 @@ fn init_subcontext_with_options( }; let fetch_locale_memo = - fetch_locale::fetch_locale(None, ssr_lang_header_getter.unwrap_or_default()); + fetch_locale::fetch_locale(None, ssr_lang_header_getter.unwrap_or_default(), None); let parent_locale = use_context::>().map(|ctx| ctx.get_locale_untracked()); @@ -439,6 +448,7 @@ macro_rules! fill_options { } #[track_caller] +#[allow(clippy::too_many_arguments)] fn provide_i18n_context_component_inner( set_lang_attr_on_html: Option, set_dir_attr_on_html: Option, @@ -446,6 +456,7 @@ fn provide_i18n_context_component_inner( cookie_name: Option>, cookie_options: Option>, ssr_lang_header_getter: Option, + parse_locale_from_path: Option>, children: impl FnOnce() -> Chil, ) -> impl IntoView { #[cfg(all(feature = "dynamic_load", feature = "hydrate", not(feature = "ssr")))] @@ -453,7 +464,7 @@ fn provide_i18n_context_component_inner( #[cfg(all(feature = "dynamic_load", feature = "ssr"))] let reg_ctx = crate::fetch_translations::RegisterCtx::::provide_context(); let options = fill_options!( - I18nContextOptions::::default(), + I18nContextOptions::::default().parse_locale_from_path(parse_locale_from_path), enable_cookie, cookie_name, cookie_options, @@ -481,6 +492,7 @@ fn provide_i18n_context_component_inner( } #[doc(hidden)] +#[allow(clippy::too_many_arguments)] #[track_caller] pub fn provide_i18n_context_component( set_lang_attr_on_html: Option, @@ -489,6 +501,7 @@ pub fn provide_i18n_context_component( cookie_name: Option>, cookie_options: Option>, ssr_lang_header_getter: Option, + parse_locale_from_path: Option>, children: TypedChildren, ) -> impl IntoView { provide_i18n_context_component_inner( @@ -498,6 +511,7 @@ pub fn provide_i18n_context_component( cookie_name, cookie_options, ssr_lang_header_getter, + parse_locale_from_path, children.into_inner(), ) } @@ -509,6 +523,7 @@ pub fn provide_i18n_context_component_island( set_dir_attr_on_html: Option, enable_cookie: Option, cookie_name: Option>, + parse_locale_from_path: Option>, children: children::Children, ) -> impl IntoView { provide_i18n_context_component_inner::( @@ -518,6 +533,7 @@ pub fn provide_i18n_context_component_island( cookie_name, None, None, + parse_locale_from_path, children, ) } diff --git a/leptos_i18n/src/fetch_locale.rs b/leptos_i18n/src/fetch_locale.rs index 88a245e0..6683baa4 100644 --- a/leptos_i18n/src/fetch_locale.rs +++ b/leptos_i18n/src/fetch_locale.rs @@ -1,21 +1,44 @@ +use std::borrow::Cow; + use leptos::prelude::*; +use leptos_router::location::{BrowserUrl, LocationProvider, RequestUrl}; use leptos_use::UseLocalesOptions; use crate::Locale; -pub fn fetch_locale(current_cookie: Option, options: UseLocalesOptions) -> Memo { +pub fn fetch_locale( + current_cookie: Option, + options: UseLocalesOptions, + parse_locale_from_path: Option>, +) -> Memo { let accepted_locales = leptos_use::use_locales_with_options(options); let accepted_locale = Memo::new(move |_| accepted_locales.with(|accepted| L::find_locale(accepted))); + + let url_locale = get_locale_from_path::(parse_locale_from_path); + if cfg!(feature = "ssr") { - fetch_locale_ssr(current_cookie, accepted_locale) + fetch_locale_ssr(current_cookie, accepted_locale, url_locale) } else if cfg!(feature = "hydrate") { fetch_locale_hydrate(current_cookie, accepted_locale) } else { - fetch_locale_csr(current_cookie, accepted_locale) + fetch_locale_csr(current_cookie, accepted_locale, url_locale) } } +fn get_locale_from_path(parse_locale_from_path: Option>) -> Option { + let base_path = parse_locale_from_path?; + let url = if cfg!(feature = "ssr") { + let req = use_context::().expect("no RequestUrl provided"); + req.parse().expect("could not parse RequestUrl") + } else { + let location = BrowserUrl::new().expect("could not access browser navigation"); + location.as_url().get_untracked() + }; + + crate::routing::get_locale_from_path(url.path(), &base_path) +} + pub fn signal_once_then( start: T, then: Memo, @@ -41,8 +64,12 @@ pub fn signal_maybe_once_then( } // ssr fetch -fn fetch_locale_ssr(current_cookie: Option, accepted_locale: Memo) -> Memo { - signal_maybe_once_then(current_cookie, accepted_locale) +fn fetch_locale_ssr( + current_cookie: Option, + accepted_locale: Memo, + url_locale: Option, +) -> Memo { + signal_maybe_once_then(url_locale.or(current_cookie), accepted_locale) } // hydrate fetch @@ -63,6 +90,10 @@ fn fetch_locale_hydrate(current_cookie: Option, accepted_locale: M } // csr fetch -fn fetch_locale_csr(current_cookie: Option, accepted_locale: Memo) -> Memo { - signal_maybe_once_then(current_cookie, accepted_locale) +fn fetch_locale_csr( + current_cookie: Option, + accepted_locale: Memo, + url_locale: Option, +) -> Memo { + signal_maybe_once_then(url_locale.or(current_cookie), accepted_locale) } diff --git a/leptos_i18n/src/macros.rs b/leptos_i18n/src/macros.rs index 1a0dab97..0a0c0f7c 100644 --- a/leptos_i18n/src/macros.rs +++ b/leptos_i18n/src/macros.rs @@ -953,7 +953,7 @@ macro_rules! tu_plural_ordinal { /// /// ```rust, ignore /// -/// `` +/// ``` #[macro_export] macro_rules! i18n_path { ($t:ty, $func:expr) => {{ diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 06a90eb6..33fcb7a6 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -217,7 +217,9 @@ fn update_path_effect( let location = use_location(); let navigate = use_navigate(); move |prev_loc: Option| { - let path_locale = get_locale_from_path::(&location, base_path); + let path_locale = location + .pathname + .with_untracked(|path| get_locale_from_path::(path, base_path)); let new_locale = i18n.get_locale(); // don't react on history change. if let Some(new_locale) = history_changed_locale.get_value() { @@ -267,7 +269,9 @@ fn correct_locale_prefix_effect( let location = use_location(); let navigate = use_navigate(); move |_| { - let path_locale = get_locale_from_path::(&location, base_path); + let path_locale = location + .pathname + .with(|path| get_locale_from_path::(path, base_path)); let current_locale = i18n.get_locale_untracked(); if current_locale == path_locale.unwrap_or_default() { @@ -309,14 +313,16 @@ fn correct_locale_prefix_effect( } } -fn get_locale_from_path(location: &Location, base_path: &'static str) -> Option { - location.pathname.with(|path| { - let stripped_path = path.strip_prefix(base_path)?; - L::get_all() - .iter() - .copied() - .find(|l| stripped_path.starts_with(l.as_str())) - }) +pub(crate) fn get_locale_from_path(path: &str, base_path: &str) -> Option { + let base_path = base_path.trim_start_matches('/'); + let stripped_path = path + .trim_start_matches('/') + .strip_prefix(base_path)? + .trim_start_matches('/'); + L::get_all() + .iter() + .copied() + .find(|l| stripped_path.starts_with(l.as_str())) } fn check_history_change( @@ -328,7 +334,9 @@ fn check_history_change( let location = use_location(); move |_| { - let path_locale = get_locale_from_path::(&location, base_path).unwrap_or_default(); + let path_locale = location + .pathname + .with_untracked(|path| get_locale_from_path::(path, base_path).unwrap_or_default()); sync.set_value(Some(path_locale)); history_changed.set_value(true); @@ -729,7 +737,7 @@ where pub fn make_i18n_segment(f: F) -> I18nSegment where L: Locale, - F: Fn(L) -> &'static str + 'static + Send + Sync, + F: Fn(L) -> &'static str + 'static + Send + Sync + Clone, { I18nSegment { func: f, diff --git a/leptos_i18n_macro/src/load_locales/mod.rs b/leptos_i18n_macro/src/load_locales/mod.rs index 115525af..afd370a9 100644 --- a/leptos_i18n_macro/src/load_locales/mod.rs +++ b/leptos_i18n_macro/src/load_locales/mod.rs @@ -200,6 +200,11 @@ fn load_locales_inner( /// Options for getting the Accept-Language header, see `leptos_use::UseLocalesOptions`. #[prop(optional)] ssr_lang_header_getter: Option, + /// Try to parse the locale from the URL pathname, expect the basepath. (default to `None`). + /// If `None` do nothing, if `Some(base_path)` strip the URL from `base_path` then expect to found a path segment that represent a locale. + /// This is usefull when using the `I18nRoute` with usage of the context outside the router. + #[prop(optional, into)] + parse_locale_from_path: Option>, children: TypedChildren ) -> impl IntoView { l_i18n_crate::context::provide_i18n_context_component::<#enum_ident, Chil>( @@ -209,6 +214,7 @@ fn load_locales_inner( cookie_name, cookie_options, ssr_lang_header_getter, + parse_locale_from_path, children ) } @@ -293,8 +299,8 @@ fn load_locales_inner( pub fn I18nRoute( /// The base path of this application. /// If you setup your i18n route such that the path is `/foo/:locale/bar`, - /// the expected base path is `/foo/`. - /// Defaults to `"/"``. + /// the expected base path is `"foo"`, `"/foo"`, `"foo/"` or `"/foo/"`. + /// Defaults to `"/"`. #[prop(default = "/")] base_path: &'static str, /// The view that should be shown when this route is matched. This can be any function From 58562c668c410a0b91268d4af09e16dd2d0be66d Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 13 Nov 2024 19:51:08 +0100 Subject: [PATCH 11/13] fixed islands with new context argument --- leptos_i18n_macro/src/load_locales/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/leptos_i18n_macro/src/load_locales/mod.rs b/leptos_i18n_macro/src/load_locales/mod.rs index afd370a9..98f11271 100644 --- a/leptos_i18n_macro/src/load_locales/mod.rs +++ b/leptos_i18n_macro/src/load_locales/mod.rs @@ -140,6 +140,11 @@ fn load_locales_inner( /// Specify a name for the cookie, default to the library default. #[prop(optional, into)] cookie_name: Option>, + /// Try to parse the locale from the URL pathname, expect the basepath. (default to `None`). + /// If `None` do nothing, if `Some(base_path)` strip the URL from `base_path` then expect to found a path segment that represent a locale. + /// This is usefull when using the `I18nRoute` with usage of the context outside the router. + #[prop(optional, into)] + parse_locale_from_path: Option>, children: Children ) -> impl IntoView { l_i18n_crate::context::provide_i18n_context_component_island::<#enum_ident>( @@ -147,6 +152,7 @@ fn load_locales_inner( set_dir_attr_on_html, enable_cookie, cookie_name, + parse_locale_from_path, children ) } From e6057629e2e84717aa4542be53d86292bdc2f712 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 13 Nov 2024 22:16:52 +0100 Subject: [PATCH 12/13] reset the current route locale after each generation --- leptos_i18n/src/routing.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/leptos_i18n/src/routing.rs b/leptos_i18n/src/routing.rs index 33fcb7a6..ca2e3f98 100644 --- a/leptos_i18n/src/routing.rs +++ b/leptos_i18n/src/routing.rs @@ -518,7 +518,7 @@ impl I18nNestedRoute { pub type BaseRoute = NestedRoute, Chil, (), View>; thread_local! { - static CURRENT_ROUTE_LOCALE: RefCell<&'static str> = const { RefCell::new("") }; + static CURRENT_ROUTE_LOCALE: RefCell> = const { RefCell::new(None) }; } #[doc(hidden)] @@ -591,7 +591,7 @@ where &'a self, path: &'a str, ) -> (Option<(leptos_router::RouteMatchId, Self::Match)>, &'a str) { - L::get_all() + let res = L::get_all() .iter() .copied() .find_map(|locale| { @@ -629,10 +629,16 @@ where (Some((route_match_id, route_match)), remaining) }) }) - .unwrap_or((None, path)) + .unwrap_or((None, path)); + reset_current_route_locale(); + res } fn generate_routes(&self) -> impl IntoIterator + '_ { + let reset = std::iter::from_fn(|| { + reset_current_route_locale(); + None + }); let default_locale_routes = std::iter::once_with(|| { set_current_route_locale(L::default()); MatchNestedRoutes::generate_routes(&self.route) @@ -660,6 +666,7 @@ where }) }) .chain(default_locale_routes) + .chain(reset) } } @@ -682,6 +689,8 @@ where segments.insert(*locale, inner_segments); } + reset_current_route_locale(); + segments } } @@ -701,12 +710,18 @@ impl Debug for I18nSegment { fn set_current_route_locale(new_locale: L) { CURRENT_ROUTE_LOCALE.with_borrow_mut(|locale| { - *locale = new_locale.as_str(); + *locale = Some(new_locale.as_str()); }) } +fn reset_current_route_locale() { + CURRENT_ROUTE_LOCALE.with_borrow_mut(|locale| *locale = None); +} + fn get_current_route_locale() -> L { - CURRENT_ROUTE_LOCALE.with_borrow(|locale| L::from_str(locale).unwrap_or_default()) + CURRENT_ROUTE_LOCALE + .with_borrow(|locale| locale.as_ref().and_then(|locale| L::from_str(locale).ok())) + .unwrap_or_default() } impl I18nSegment From 1e789d40f94653f98e4d30b542d73dee51efaf47 Mon Sep 17 00:00:00 2001 From: Baptistemontan Date: Wed, 13 Nov 2024 22:17:01 +0100 Subject: [PATCH 13/13] update docs --- docs/book/src/usage/07_router.md | 92 ++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/docs/book/src/usage/07_router.md b/docs/book/src/usage/07_router.md index e80babe4..f4855a1c 100644 --- a/docs/book/src/usage/07_router.md +++ b/docs/book/src/usage/07_router.md @@ -12,10 +12,10 @@ use leptos_router::*; view! { - - - - + + + + @@ -50,7 +50,8 @@ Switching locale updates the prefix accordingly, switching from `en` to `fr` wil Switching locale will trigger a navigation, update the `Location` returned by `use_location`, but will not refresh the component tree. -This means that if `Counter` keep a count as a state, and you switch locale from `fr` to `en`, this will trigger a navigation from `"/fr/counter"` to `"/counter"`, but the component will not be rerendered. +This means that if `Counter` keep a count as a state, and you switch locale from `fr` to `en`, this will trigger a navigation from `"/fr/counter"` to `"/counter"`, +but the component will not be rerendered and the count state will be preserved. ## Navigation @@ -61,3 +62,84 @@ This redirection also set `NavigateOptions.replace` to `true` so the intermediat Basically, if you are at `"/fr/counter"` and trigger a redirection to `"/"`, this will trigger another redirection to `"/fr"` and the history will look like you directly navigated from `"/fr/counter"` to `"/fr"`. + +## Localized path segments + +You can use inside the `i18nRoute` the `i18n_path!` to create localized path segments: + +```rust +use leptos_i18n::i18n_path; + + + + +``` + +If you have `segment_path_name = "search"` for english, and `segment_path_name = "rechercher"` for french, the `I18nRoute` will produce 3 paths: + +- "/search" (if default = "en") +- "/en/search" +- "/fr/rechercher" + +It can be used at any depth, and if not used inside a `i18nRoute` it will default to the default locale. + +## Caveat + +If you have a layout like this: + +```rust +view! { + + + + + + + + + + +} +``` + +And the `Menu` component use localization, you could be suprised to see that sometimes there is a mismatch beetween the locale used by the `Menu` and the one inside the router. +This is due to the locale being read from the URL only when the `i18nRoute` is rendered. So the context may be initialized with another locale, and then hit the router that update it. + +You have multiple solutions, you can either use the `Menu` component inside the `i18nRoute`: + +```rust +view! { + + + + + + }> + + + + + +} +``` + +Or you can use the `parse_locale_from_path` option on the `I18nContextProvider`: + +```rust +view! { + + + + + + + + + + +} +``` + +This option force the context to initialize itself with the locale from the URL. It is not enabled by default because the only time it is neededis this particular case. +It expect the base_path argument you would pass to the `I18nRoute`.