mondeja / leptos-fluent

Internationalization framework for Leptos using Fluent
https://mondeja.github.io/leptos-fluent/
MIT License
38 stars 10 forks source link

Issue with leptos_fluent and Islands feature in Leptos #245

Closed NCura closed 1 month ago

NCura commented 1 month ago

Hello,

I'm trying to implement islands in one of my websites, and I've encountered an issue while using leptos_fluent with the islands feature. I’ve already opened an issue in the main Leptos repo regarding my initial problem, and after resolving it, I ran into this issue with leptos_fluent.

Here’s what I’ve done so far:

leptos-fluent = { version = "0.1.21", features = ["nightly"] }
fluent-templates = { version = "0.11.0", default-features = false, features = ["macros", "walkdir"] }
static_loader! {
    static TRANSLATIONS = {
        locales: "./locales",
        fallback_language: "en",
    };
}

pub static COMPOUND: &[&Lazy<StaticLoader>] = &[&TRANSLATIONS, &TRANSLATIONS];

And inside the App() function:

let i18n = leptos_fluent! {
    translations: [TRANSLATIONS, TRANSLATIONS] + COMPOUND,
    languages: "./locales/languages.json",
    locales: "./locales",
    sync_html_tag_lang: true,
    sync_html_tag_dir: true,
    cookie_name: "lang",
    cookie_attrs: "SameSite=Strict; Secure; path=/; max-age=600",
    initial_language_from_cookie: true,
    initial_language_from_cookie_to_localstorage: true,
    set_language_to_cookie: true,
    url_param: "lang",
    initial_language_from_url_param: true,
    initial_language_from_url_param_to_localstorage: true,
    initial_language_from_url_param_to_cookie: true,
    set_language_to_url_param: true,
    localstorage_key: "language",
    initial_language_from_localstorage: true,
    initial_language_from_localstorage_to_cookie: true,
    set_language_to_localstorage: true,
    initial_language_from_navigator: true,
    initial_language_from_navigator_to_localstorage: true,
    initial_language_from_accept_language_header: true,
};
provide_context(i18n);
#[island]
fn HomePage() -> impl IntoView {
    let i18n = use_context::<I18n>().expect("to have found the i18n provided in App");
    let (count, set_count) = create_signal(0);
    let on_click = move |_| set_count.update(|count| *count += 1);

    view! {
        <button on:click=on_click>{move_tr!(i18n, "click-me")}" "{count}</button>
    }
}

However, when I visit the website, I get the following error in the browser console, and the button no longer works:

Uncaught (in promise) RuntimeError: unreachable
    at test_islands.wasm.__rust_start_panic (test-islands.wasm:0x1ec288)
    ...

I’ve managed to make it work by replacing, in HomePage,

let i18n = use_context::<I18n>().expect("to have found the i18n provided in App");

with

let i18n = leptos_fluent! {
  translations: [TRANSLATIONS, TRANSLATIONS] + COMPOUND,
  languages: "./locales/languages.json",
  locales: "./locales"
};

However, this creates a new i18n instance, which differs from the one provided in App, making it inconvenient to manage translations consistently. (I can even remove the i18n parameter from the move_tr!, and simply use the leptos_fluent! macro without assigning it to a variable.)

Do you have any suggestions on how to resolve this? I’d be happy to submit a PR with an example of leptos_fluent working alongside islands if we can find a good solution.

Thanks in advance for your help!

mondeja commented 1 month ago

Is the expected behaviour. In islands mode, only #[island] contents are interactive. The unreachable error is produced because you're trying to access from the client to a context that was provided in the server, so the context doesn't exists.

However, this creates a new i18n instance, which differs from the one provided in App

This statement is not totally true. Your code creates an instance on the server (inside App) and another instance is shipped to and instanciated in the client (in HomePage). It creates 2 instances on 2 different runs, one in the server and one in the client.

You'll probably end shipping a lot of translations to the client if you use the same ./locales directory. Just use different locales for the translations of each island. Note that this approach that leptos-fluent uses of allowing multiple cheap instances with a macro fits perfectly for the islands mode.

Another thing that I think that you're concerned about is how to define the same arguments for both instances. Well, note that some of the arguments only have sense for the server and others for the client. As examples, sync_html_tag_lang is ignored on server (inside #[component]) and initial_language_from_accept_language_header is ignored on client (inside #[island]). So is an imaginary problem.

Note in your example that you're providing the i18n context, but leptos_fluent! do that internally anyways. These two are equivalent:

leptos_fluent!{ ... };
let i18n = leptos_fluent!{ ... };
provide_context(i18n);

If you want the same instance parameters in all islands, just provide one with leptos_fluent! in a parent island and the context will be available to their children (see Passing Context Between Islands). If you want one i18n instance per island defining a different locales directory and maintaining the same arguments for all other island invocations, wrap the leptos_fluent! call in a macro, something like:

macro_rules! i18n {
    ($translations:expr$(,)?) => {
        leptos_fluent! {
            translations: [$translations],
            // ... rest of your arguments
        }
    };
}

Probably there are bugs or quirks that I'm not aware about because I didn't used this mode so much. Feel free to open a PR with an island example or documentation about that, it would be really useful for other users.

NCura commented 1 month ago

I've submitted a pull request with an islands example. While there are still some areas for improvement, it provides a foundation to build upon.