Skip to content

Commit

Permalink
Alternative to panicking outside reactive ownership tree (#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
mondeja authored Aug 19, 2024
1 parent 40ec9f3 commit d72b49c
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 20 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions book/src/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions book/src/faqs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
<Show when=|| true>
<Child/>
</Show>
}
}

#[component]
pub fn Child() -> impl IntoView {
leptos_fluent! {{
// ...
}};
view! {
<div on:click=|_| {
tr!("my-translation");
}>"CLICK ME!"</div>
}
}
```

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! {
<Show when=|| true>
<Child/>
</Show>
}
}

#[component]
pub fn Child() -> impl IntoView {
let i18n = leptos_fluent! {{
// ...
}};
view! {
<div on:click=|_| {
tr!(i18n, "my-translation");
}>"CLICK ME!"</div>
}
}
```

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 [`<For/>`] component?

```admonish bug
Expand Down
73 changes: 73 additions & 0 deletions end2end/tests/context_outside_reactive_ownership_tree.rs
Original file line number Diff line number Diff line change
@@ -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! {
<Show when=|| true>
<Child/>
</Show>
}
}

#[component]
fn Child() -> impl IntoView {
use wasm_bindgen::JsCast;
leptos_fluent! {{
translations: [TRANSLATIONS],
locales: "../examples/csr-minimal/locales",
}};
view! {
<div
id="fails"
on:click=|ev| {
expect_i18n();
ev.target()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>()
.set_inner_text("CLICKED!");
}
>

"CLICK ME!"
</div>
<div
id="success"
on:click=|ev| {
ev.target()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>()
.set_inner_text("CLICKED!");
}
>

"CLICK ME!"
</div>
}
}

#[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!();
}
4 changes: 2 additions & 2 deletions examples/csr-complete/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ pub fn App() -> impl IntoView {
set_language_from_navigator: true,
}};

view! { <ChildComponent/> }
LanguageSelector
}

#[component]
fn ChildComponent() -> impl IntoView {
fn LanguageSelector() -> impl IntoView {
let i18n = expect_i18n();

view! {
Expand Down
2 changes: 1 addition & 1 deletion examples/csr-minimal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub fn App() -> impl IntoView {
locales: "./locales",
}};

view! { <LanguageSelector/> }
LanguageSelector
}

#[component]
Expand Down
2 changes: 1 addition & 1 deletion leptos-fluent-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions leptos-fluent-macros/src/translations_checker/tr_macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
),
]
);
}
}
2 changes: 1 addition & 1 deletion leptos-fluent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 41 additions & 12 deletions leptos-fluent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,16 +545,26 @@ pub fn use_i18n() -> Option<I18n> {
use_context::<I18n>()
}

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)]
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)
Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -618,14 +626,15 @@ 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, FluentValue>,
) -> String {
let I18n {
language,
translations,
..
} = expect_i18n();
} = i18n;
let found = with!(|translations, language| {
translations
.iter()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))]
Expand Down

0 comments on commit d72b49c

Please sign in to comment.