diff --git a/CHANGELOG.md b/CHANGELOG.md index 60733cb4..7e200f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # CHANGELOG +## 2024-08-19 - [0.1.19] + +### Bug fixes + +- Allow to pass `i18n` as first argument to `tr!` and `move_tr!` macros. + This is an alternative to panicking when using the macros in event handlers. + ## 2024-08-18 - [0.1.18] +### Bug fixes + - Relax `fluent-templates` dependency. ## 2024-08-17 - [0.1.17] @@ -484,6 +493,7 @@ version to `0.1` during installation. - Added all ISO-639-1 and ISO-639-2 languages. +[0.1.19]: https://github.com/mondeja/leptos-fluent/compare/v0.1.18...v0.1.19 [0.1.18]: https://github.com/mondeja/leptos-fluent/compare/v0.1.17...v0.1.18 [0.1.17]: https://github.com/mondeja/leptos-fluent/compare/v0.1.16...v0.1.17 [0.1.16]: https://github.com/mondeja/leptos-fluent/compare/v0.1.15...v0.1.16 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60d214ed..05331b60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ cargo install wasm-pack You need to install a browser and run: ```sh -cd tests +cd end2end wasm-pack test --{browser} --headless ``` diff --git a/Cargo.lock b/Cargo.lock index 01c4e2e6..15db20a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,7 +1799,7 @@ dependencies = [ [[package]] name = "leptos-fluent" -version = "0.1.18" +version = "0.1.19" dependencies = [ "current_locale", "directories", @@ -1848,7 +1848,7 @@ dependencies = [ [[package]] name = "leptos-fluent-macros" -version = "0.1.18" +version = "0.1.19" dependencies = [ "cfg-expr 0.15.8", "current_platform", diff --git a/book/src/basic-usage.md b/book/src/basic-usage.md index 341ccc53..1a3926dc 100644 --- a/book/src/basic-usage.md +++ b/book/src/basic-usage.md @@ -248,6 +248,8 @@ To check if a language is the active one, use [`is_active`] method of a ```rust lang.is_active() + +lang == expect_i18n().language.get() ``` [`tr!`]: https://docs.rs/leptos-fluent/latest/leptos_fluent/macro.tr.html diff --git a/book/src/faqs.md b/book/src/faqs.md index 0cb797ba..6b3fb022 100644 --- a/book/src/faqs.md +++ b/book/src/faqs.md @@ -54,6 +54,70 @@ From fluent-templates `v0.10` onwards can be obtained from your translations. let fallback_language = expect_i18n().translations.get()[0].fallback(); ``` +### Using `tr!` and `move_tr!` macros on event panics + +The i18n context can't be obtained from outside the reactive ownership tree. +This means there are certain locations where we can't use `tr!("my-translation")`, +like inside `on:` events. For example, the next code panics: + +```rust +#[component] +pub fn App() -> impl IntoView { + view! { + + + + } +} + +#[component] +pub fn Child() -> impl IntoView { + leptos_fluent! {{ + // ... + }}; + view! { +
"CLICK ME!"
+ } +} +``` + +With Leptos v0.7, whatever `tr!` macro used in the `on:` event will panic, +but with Leptos v0.6, this outsiding of the ownership tree has been ignored +from the majority of the cases as unintended behavior. + +To avoid that, pass the i18n context as first parameter to `tr!` or `move_tr!`: + +```rust +#[component] +pub fn App() -> impl IntoView { + view! { + + + + } +} + +#[component] +pub fn Child() -> impl IntoView { + let i18n = leptos_fluent! {{ + // ... + }}; + view! { +
"CLICK ME!"
+ } +} +``` + +And shortcuts cannot be used. Rewrite all the code that calls `expect_context` +internally: + +- Use `i18n.language.set(lang)` instead of `lang.activate()`. +- Use `lang == i18n.language.get()` instead of `lang.is_active()`. + ### Why examples don't use [``] component? ```admonish bug diff --git a/end2end/tests/context_outside_reactive_ownership_tree.rs b/end2end/tests/context_outside_reactive_ownership_tree.rs new file mode 100644 index 00000000..09041f9c --- /dev/null +++ b/end2end/tests/context_outside_reactive_ownership_tree.rs @@ -0,0 +1,73 @@ +/// See: +/// - https://github.com/leptos-rs/leptos/issues/2852 +/// - https://github.com/mondeja/leptos-fluent/issues/231 +use leptos::*; +use leptos_fluent::{expect_i18n, leptos_fluent}; +use leptos_fluent_csr_minimal_example::TRANSLATIONS; +use tests_helpers::{input_by_id, mount, unmount}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[component] +fn App() -> impl IntoView { + view! { + + + + } +} + +#[component] +fn Child() -> impl IntoView { + use wasm_bindgen::JsCast; + leptos_fluent! {{ + translations: [TRANSLATIONS], + locales: "../examples/csr-minimal/locales", + }}; + view! { +
() + .set_inner_text("CLICKED!"); + } + > + + "CLICK ME!" +
+
() + .set_inner_text("CLICKED!"); + } + > + + "CLICK ME!" +
+ } +} + +#[wasm_bindgen_test] +async fn context_outise_reactive_ownership_tree() { + let fails_div = move || input_by_id("fails"); + let success_div = move || input_by_id("success"); + + mount!(App); + assert_eq!(fails_div().inner_text(), "CLICK ME!"); + fails_div().click(); + assert_eq!(fails_div().inner_text(), "CLICK ME!"); + unmount!(); + + mount!(App); + assert_eq!(success_div().inner_text(), "CLICK ME!"); + success_div().click(); + assert_eq!(success_div().inner_text(), "CLICKED!"); + unmount!(); +} diff --git a/examples/csr-complete/src/lib.rs b/examples/csr-complete/src/lib.rs index 7af64869..b7eba183 100644 --- a/examples/csr-complete/src/lib.rs +++ b/examples/csr-complete/src/lib.rs @@ -38,11 +38,11 @@ pub fn App() -> impl IntoView { set_language_from_navigator: true, }}; - view! { } + LanguageSelector } #[component] -fn ChildComponent() -> impl IntoView { +fn LanguageSelector() -> impl IntoView { let i18n = expect_i18n(); view! { diff --git a/examples/csr-minimal/src/lib.rs b/examples/csr-minimal/src/lib.rs index 3951bd30..f2c0de1a 100644 --- a/examples/csr-minimal/src/lib.rs +++ b/examples/csr-minimal/src/lib.rs @@ -16,7 +16,7 @@ pub fn App() -> impl IntoView { locales: "./locales", }}; - view! { } + LanguageSelector } #[component] diff --git a/leptos-fluent-macros/Cargo.toml b/leptos-fluent-macros/Cargo.toml index d888f616..97e7a09c 100644 --- a/leptos-fluent-macros/Cargo.toml +++ b/leptos-fluent-macros/Cargo.toml @@ -2,7 +2,7 @@ name = "leptos-fluent-macros" description = "Macros for leptos-fluent" edition.workspace = true -version = "0.1.18" +version = "0.1.19" license = "MIT" documentation.workspace = true repository.workspace = true diff --git a/leptos-fluent-macros/src/translations_checker/tr_macros.rs b/leptos-fluent-macros/src/translations_checker/tr_macros.rs index f3fddc45..a24e3eee 100644 --- a/leptos-fluent-macros/src/translations_checker/tr_macros.rs +++ b/leptos-fluent-macros/src/translations_checker/tr_macros.rs @@ -597,4 +597,27 @@ mod tests { ] ); } + + #[test] + fn context_as_first_macro_parameters() { + let content = quote! { + fn App() -> impl IntoView { + tr!(i18n, "select-a-language"); + tr!(i18n, "html-tag-lang-is", { "foo" => "value1", "bar" => "value2" }); + } + }; + let tr_macros = tr_macros_from_file_content(&content.to_string()); + + assert_eq!( + tr_macros, + vec![ + tr_macro!("tr", "select-a-language", Vec::new()), + tr_macro!( + "tr", + "html-tag-lang-is", + vec!["foo".to_string(), "bar".to_string()] + ), + ] + ); + } } diff --git a/leptos-fluent/Cargo.toml b/leptos-fluent/Cargo.toml index 2a134a78..1d841d30 100644 --- a/leptos-fluent/Cargo.toml +++ b/leptos-fluent/Cargo.toml @@ -2,7 +2,7 @@ name = "leptos-fluent" description = "Fluent framework for internationalization of Leptos applications" edition.workspace = true -version = "0.1.18" +version = "0.1.19" license = "MIT" documentation.workspace = true repository.workspace = true diff --git a/leptos-fluent/src/lib.rs b/leptos-fluent/src/lib.rs index f9360ddf..14450d24 100644 --- a/leptos-fluent/src/lib.rs +++ b/leptos-fluent/src/lib.rs @@ -545,6 +545,18 @@ pub fn use_i18n() -> Option { use_context::() } +const EXPECT_I18N_ERROR_MESSAGE: &str = concat!( + "I18n context is missing, use the `leptos_fluent!` macro to provide it.\n\n", + "If you're sure that the context has been provided probably the invocation", + " resides outside of the reactive ownership tree, thus the context is not", + " reachable. Use instead:\n", + " - `tr!(i18n, \"text-id\")` instead of `tr!(\"text-id\")`.\n", + " - `move_tr!(i18n, \"text-id\")` instead of `move_tr!(\"text-id\")`.\n", + " - `i18n.language.set(lang)` instead of `lang.activate()`.\n", + " - `lang == i18n.language.get()` instead of `lang.is_active()`.\n", + " - Copy `i18n` context instead of getting it `expect_i18n()`.", +); + /// Expect the current context for localization. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))] #[inline(always)] @@ -552,9 +564,7 @@ pub fn expect_i18n() -> I18n { if let Some(i18n) = use_i18n() { i18n } else { - let error_message = concat!( - "I18n context is missing, use the leptos_fluent! macro to provide it." - ); + let error_message = EXPECT_I18N_ERROR_MESSAGE; #[cfg(feature = "tracing")] tracing::error!(error_message); panic!("{}", error_message) @@ -568,9 +578,7 @@ pub fn i18n() -> I18n { if let Some(i18n) = use_i18n() { i18n } else { - let error_message = concat!( - "I18n context is missing, use the leptos_fluent! macro to provide it." - ); + let error_message = EXPECT_I18N_ERROR_MESSAGE; #[cfg(feature = "tracing")] tracing::error!(error_message); panic!("{}", error_message) @@ -579,12 +587,12 @@ pub fn i18n() -> I18n { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[doc(hidden)] -pub fn tr_impl(text_id: &str) -> String { +pub fn tr_impl(i18n: I18n, text_id: &str) -> String { let I18n { language, translations, .. - } = expect_i18n(); + } = i18n; let found = with!(|translations, language| { translations .iter() @@ -618,6 +626,7 @@ pub fn tr_impl(text_id: &str) -> String { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[doc(hidden)] pub fn tr_with_args_impl( + i18n: I18n, text_id: &str, args: &std::collections::HashMap, ) -> String { @@ -625,7 +634,7 @@ pub fn tr_with_args_impl( language, translations, .. - } = expect_i18n(); + } = i18n; let found = with!(|translations, language| { translations .iter() @@ -667,16 +676,26 @@ pub fn tr_with_args_impl( /// ``` #[macro_export] macro_rules! tr { - ($text_id:literal$(,)?) => {::leptos_fluent::tr_impl($text_id)}; + ($text_id:literal$(,)?) => {$crate::tr_impl($crate::expect_i18n(), $text_id)}; ($text_id:literal, {$($key:literal => $value:expr),*$(,)?}$(,)?) => {{ - ::leptos_fluent::tr_with_args_impl($text_id, &{ + $crate::tr_with_args_impl($crate::expect_i18n(), $text_id, &{ let mut map = ::std::collections::HashMap::new(); $( map.insert($key.to_string(), $value.into()); )* map }) - }} + }}; + ($i18n:expr, $text_id:literal$(,)?) => {$crate::tr_impl($i18n, $text_id)}; + ($i18n:expr, $text_id:literal, {$($key:literal => $value:expr),*$(,)?}$(,)?) => {{ + $crate::tr_with_args_impl($i18n, $text_id, &{ + let mut map = ::std::collections::HashMap::new(); + $( + map.insert($key.to_string(), $value.into()); + )* + map + }) + }}; } /// [`leptos::Signal`] that translates a text identifier to the current language. @@ -712,6 +731,16 @@ macro_rules! move_tr { )* })) }; + ($i18n:expr, $text_id:literal$(,)?) => { + ::leptos::Signal::derive(move || $crate::tr!($i18n, $text_id)) + }; + ($i18n:expr, $text_id:literal, {$($key:literal => $value:expr),*$(,)?}$(,)?) => { + ::leptos::Signal::derive(move || $crate::tr!($i18n, $text_id, { + $( + $key => $value, + )* + })) + }; } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]