-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Islands example #246
Islands example #246
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for opening the PR.
To make the example more real, please, use a separate translations directory for the "click-me"
and load in other TRANSLATIONS
object named ISLAND_TRANSLATIONS
as this identifier is the unique used by the island. Currently, the other "welcome-to-leptos"
identifier would be included in the code shipped to the client to hydrate the island.
Because
Remember, one context per archipelago or island. |
…_static_file implementation
I'm not entirely sure I understand the intended workflow for managing multiple translation directories. Currently, I’ve structured the Right now, I’m using translations in a Would this structure work for that? - locales
- core
- en
- es
- server
- en
- es
- island-home
- en
- es
- island-contact
- en
- es
- languages.json
Does this align with the correct workflow? |
The current implementation gives an error in the browser console when trying to switch languages, complaining about |
I've pushed a fix for the example. Does it resolve your questions?
This is a misunderstanding about what |
examples/ssr-islands-axum/src/app.rs
Outdated
// Children will be executed when the i18n context is ready. | ||
// See https://book.leptos.dev/islands.html#passing-context-between-islands | ||
// | ||
// > That’s why in `HomePage`, I made `let tabs = move ||` a function, and | ||
// > called it like `{tabs()}`: creating the tabs lazily this way meant that | ||
// > the `Tabs` island would already have provided the `selected` context by | ||
// > the time each `Tab` went looking for it. | ||
children() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a problem with the way I created the parent island and added the children islands?
This is the answer. See the final note in Passing Context Between Islands:
That’s why in
HomePage
, I madelet tabs = move ||
a function, and called it like{tabs()}
: creating the tabs lazily this way meant that theTabs
island would already have provided theselected
context by the time eachTab
went looking for it.
Is really tricky and very contraintuitive.
Just realize that when you set a <Component/>
in a view!
, you're not instantiating an object, you're just creating an AST node that will be rendered who knows when and in what conditions. In this example, seems like we're instantiating <Counter/>
and <LanguageSelector/>
in the server, but is not the case.
Thank you for fixing the example! I've added more content to simulate a common setup for multilanguage sites, where the language selector is often placed in the header, footer, or both. I thus introduced some nested routes.
That’s the current layout of the example, but now I need to make it work as expected. At present, when the language is changed, only the text within the same island as the Do you have any suggestions on how to ensure that changing the language via the |
examples/ssr-islands-axum/src/app.rs
Outdated
let i18n = expect_i18n(); | ||
view! { | ||
<header> | ||
<A href="/">{move_tr!("home")}</A> | ||
<LanguageSelector/> | ||
<A href="/page-2">{move_tr!("page-2")}</A> | ||
<A href="/">{move_tr!(i18n, "home")}</A> | ||
<LanguageSelector /> | ||
<A href="/page-2">{move_tr!(i18n, "page-2")}</A> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- In the
header
module, I added some navigation links along with theLanguageSelector
, as most users would typically place these in the header. The links don’t need to be inside an island, since they can inherit the i18n context fromApp()
. To test behavior, I placed one link before theLanguageSelector
and one after. I expected both links to be translated, but only the first one is. TheLanguageSelector
loads its own context, which I assumed would remain confined to the island, without affecting translations from the#[component]
running on the server.
This is another unintuitive case of provide_context
/ use_context
behaviour. In Leptos v0.6, contexts are herratically spread from child components in a declarative way, but v0.7 "improves" (?) it by executing the child first and spreading the last to their parents. In v0.6, the first string is translated but in v0.7 none of them are translated.
I'm in the team that components should be always declared using <Context/>
components to avoid these kinds of strange behaviours. Even I would remove use_context
at all in the future from Leptos itself, looks like the root of all evils. In leptos-fluent v0.2 you'll be forced to pass a children
prop leptos_fluent!
because internally it will use <Context/>
.
To specify exactly what i18n context to use, you can pass it as the first value to move_tr!
and tr!
macros.
c8747ef
to
0875475
Compare
I think that you're not completely aware about how the reactive graph works. Is not the same as the component tree. Consider the next example: #[component]
fn Foo() -> impl IntoView {
provide_context::<usize>(0);
view! {
<h1>"Foo"</h1>
{
let value = expect_context::<usize>();
view! {
<p>"Context value before Bar: "{value}</p>
}
}
<Bar/>
{
let value = expect_context::<usize>();
view! {
<p>"Context value after Bar -> Baz: "{value}</p>
}
}
}
}
#[component]
fn Bar() -> impl IntoView {
provide_context::<usize>(1);
view! {
<h1>"Bar"</h1>
{
let value = expect_context::<usize>();
view! {
<p>"Context value before Baz: "{value}</p>
}
}
<Baz/>
}
}
#[component]
fn Baz() -> impl IntoView {
provide_context::<usize>(2);
view! {
<h1>"Baz"</h1>
}
} What should it render? Well... it renders this: <h1>Foo</h1>
<p>Context value before Bar: <!---->0</p>
<h1>Bar</h1>
<p>Context value before Baz: <!---->1</p>
<h1>Baz</h1>
<p>Context value after Bar -> Baz: <!---->2</p> Because This is not intuitive at a first glance. It is what is happening in your examples. Just pass an explicit |
Thank you for the clarifications, and for specifying the need to pass the i18n context to every I'm currently focusing on the header translation: #[component]
pub fn View() -> impl IntoView {
let i18n = expect_i18n();
view! {
<header>
<A href="/">{move_tr!(i18n, "home")}</A>
<LanguageSelector />
<A href="/page-2">{move_tr!(i18n, "page-2")}</A>
</header>
}
}
#[island]
fn LanguageSelector() -> impl IntoView {
let i18n = i18n!([TRANSLATIONS], "./locales/header");
view! {
<div style="display: inline-flex; margin-left: 10px">
{move_tr!(i18n, "select-language")} ": "
{move || {
i18n.languages
.iter()
.map(|lang| {
view! {
<div>
<input
type="radio"
id=lang
name="language"
value=lang
checked=lang.is_active()
on:click=move |_| i18n.language.set(lang)
/>
<label for=lang>{lang.name}</label>
</div>
}
})
.collect::<Vec<_>>()
}}
</div>
}
} The goal is to update the text inside the I’ve attempted several approaches, including the four patterns outlined here. However, none have worked because the I also tried saving the selected language to on:click=move |_| {
i18n.language.set(lang);
leptos_fluent::web_sys::window()
.unwrap()
.local_storage()
.unwrap()
.unwrap()
.set_item("selected_language", lang.name)
.unwrap();
} But I encountered issues retrieving it inside the Do you have any suggestions on how we can achieve this? |
Why are you using a Remember: |
… new example with all translations on the server
This is the internal debate I’ve been having: On the one hand, islands are use for interactive pieces, and I would like the translations to react to the language change. On the other hand, they should be as small and specific as possible. However, if I have a fully translated website with little backend code, it seems I would need an archipelago containing almost all of my components just to share an i18n context. At that point, I feel like I'm losing the size reduction benefits of using islands. Am I missing something? The best compromise I’ve come up with, when using I’ve used the original example to demonstrate the first approach, where we have a single archipelago that shares the i18n context across components. I made some necessary adjustments based on a bug mentioned by
So I had to refactor the code: #[island]
pub fn HeaderView() -> impl IntoView {
view! {
<header>
<a href="/">{move_tr!("home")}</a>
<a href="/page-2">{move_tr!("page-2")}</a>
<LanguageSelector/>
</header>
}
} into: #[component]
pub fn HeaderView() -> impl IntoView {
view! {
<header>
<HeaderLinks/>
<LanguageSelector/>
</header>
}
}
#[island]
fn HeaderLinks() -> impl IntoView {
view! {
<a href="/">{move_tr!("home")}</a>
<a href="/page-2">{move_tr!("page-2")}</a>
}
} I did similar changes in the I also added a second example, keeping the translations on the server. The downside is that the page must reload when changing the language, as mentioned before. The upside is that the wasm size is reduced to 282k, and this size won’t grow even as more content is added to the site. In contrast, the first example (382k) will continue to increase in size with additional content. Would it make sense to include both examples (or perhaps one example but with explanations for both approaches)? This way, users of |
I think that should be added just one example for islands using the second approach with page reloads which is the one that has more sense with islands because exploits better the benefit of file size, the main goal of islands design. IMHO, including both examples would add unnecessary noise for users who consult them. To provide more context, I would suggest you include an explanation of this lack of interactivity issue in the README and/or in comments within the code, probably where you trigger the site reload. |
That sounds great to me! I’ll implement this tomorrow. As an example, I’ve already updated one of my client’s websites from the first approach to the second in the development environment. The result was a significant reduction in the WebAssembly size, going from just over 1000KB down to less than 400KB. Given this improvement, the trade-off of a page reload is definitely worth it. |
Additional Changes to the Final Example
Request for ClarificationIn the i18n context setup within both
Could you help clean up the arguments in the Follow-up Questions
|
Cleaned.
Yes, we should use
Because all
That's the question with islands and I think that leptos-fluent is not enough featured to allow this for now. However, there is a really dirty workaround that we can apply now to the example. The most simple solution when you don't need interactivity would be to pass translations as island arguments. Something like: #[component]
fn MyServerComponent() -> impl IntoView {
view! {
<MyIsland translated_message=tr!("foo") />
}
}
#[island]
fn MyIsland(translated_message: String) -> impl IntoView {
view! {
<p>{translated_message}</p>
}
} In the case of this example is not enough. Even if we pass a Currently That would solve the problem for this example by using something like: #[component]
fn MyServerComponent() -> impl IntoView {
view! {
<MyArchipelago translations=HashMap::from([
("foo", tr!("foo")),
("bar", tr!("bar")),
]) />
}
}
#[island]
fn MyArchipelago(translations: HashMap<String, String>) -> impl IntoView {
provide_context(translations)
view! {
<MyIsland />
}
}
#[island]
fn MyIsland() -> impl IntoView {
let tr = expect_context::<HashMap<String, String>>();
view! {
<p>{tr.get("foo").unwrap()}</p>
<p>{tr.get("bar").unwrap()}</p>
}
} And though it works, the syntax looks weird. Other current workaround is to duplicate the repeated keys in a separated locales folder, but that is not convenient. Another possible solution would be to implement some kind of option to |
I saw the PR allowing the creation of an i18n context with an empty array — awesome! In this case, given that some translations are required on the client for the header, I added a However, the issue now is that the texts in the To fix this, I called the i18n context function from #[component]
pub fn View() -> impl IntoView {
view! {
<header>
<Archipelago>
<LargeMenu />
<MobileMenu />
</Archipelago>
{super::provide_i18n_context();}
</header>
}
} If you think that there is a better place to call the context again, feel free to tell me. Using it in What do you think of this solution? It seems to be working as expected and prevents the server-side translations from being sent to the client, but I'd appreciate your thoughts in case I’m overlooking something. P.D.: I've kept the test translation text inside the |
If you call |
Okay, got it! |
Awesome. Thank you for the great work! |
Hello,
This is my current implementation of an islands example. I've started with the Axum template, integrated the islands feature, and added the
leptos_fluent
dependencies. Inapp.rs
, I introduced a macro to maintain consistent arguments across the application, which is called within theApp()
function.To better reflect a real-world scenario, I created a new module instead of keeping everything in a single file. The module includes a
HomePage()
component, which utilizes themacro_tr!
. Since it's a#[component]
, it automatically has access to the i18n context.There are also two islands, each requiring its own i18n context. I modified the
LanguageSelector
to use the i18n context that was explicitly created, rather than relying on shortcuts that assume the context is already available.The current setup is functional, but I see a few areas for improvement:
LanguageSelector
differs from the one inCounter
. The language change needs to be broadcasted to all other parts of the site where translations are used, such asCounter
,HomePage
, and evenApp
for updating theTitle
.[TRANSLATIONS, TRANSLATIONS] + COMPOUND
is being used instead of just[TRANSLATIONS]
, as demonstrated in the translations example from the Leptos Fluent documentation.i18n!
macro to take no parameters, embedding the Translations array directly within the macro rules? This would eliminate the need to invokestatic_loader!
in every file that uses translations.I'm happy to make further changes based on any suggestions or feedback!