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))]