Skip to content
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

Lazy loading of locale data #88

Closed
Boscop opened this issue Jan 12, 2024 · 20 comments · Fixed by #182
Closed

Lazy loading of locale data #88

Boscop opened this issue Jan 12, 2024 · 20 comments · Fixed by #182
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@Boscop
Copy link

Boscop commented Jan 12, 2024

Is it possible to do lazy loading of locale data?
Locale data can be huge depending on the number of keys and languages, and usually only 1 language's values are needed at runtime.
So a lot can be saved by lazy loading (which is why e.g. i18next supports it).

@alilee
Copy link

alilee commented Jan 14, 2024

locale should be a Leptos resource.

@Baptistemontan
Copy link
Owner

Baptistemontan commented Jan 17, 2024

This is something I would want to have too, but the main idea I had making this library was compile time check for keys and values. To implement something like this there is multiple solutions, but most of them would need an almost complete rewrite and rethink and I'm not in a position to do that for now. The closest solution I could come up with without too much change would be to compile the client for evey locale and send the correct one, or to make the i18n module some kind of dynamic library to could be loaded on the client. I would be happy to explore those possibilities but I have very limited time lately due to personnal reasons, I can't guarantee you anything for now.

@Boscop
Copy link
Author

Boscop commented Jan 21, 2024

@Baptistemontan

the main idea I had making this library was compile time check for keys and values.

That's orthogonal to lazy loading: It can still compile-time check keys & values but lazy-load language packs at runtime.
Just like in a previous project where I was using i18next in a TypeScript frontend, I had a Rust backend, and in its build.rs I did compile-time checking of keys & values. But the frontend lazy-loaded only the required language's json file.

The closest solution I could come up with without too much change would be to compile the client for evey locale and send the correct one

This would not be suitable for any app where the UI language changes at runtime, i.e. every app where the user can set the desired UI language.
(Also, the value of the Accept-Language header is not always one language, it might be multiple or *. And the UI language that the user has set in their settings doesn't necessarily match this Accept-Language header value!)

The solution would be to do it like i18next: The backend serves the different <lang>.json files and the frontend loads one on demand, and loads another one if the UI language setting changes.
This doesn't prevent the build script from doing any compile-time checks, because the content of those json files doesn't change after compilation :)

@Baptistemontan
Copy link
Owner

Alright I've been thinking about it lately and made a very barebone POC, and I have some promising ideas on how to implement that, it will be a big rewrite but will be worth it. I will make this the core new feature of v0.4.

@Baptistemontan
Copy link
Owner

I'm working on it right now, you can follow #97 to see the advancement, but I would like to have a some project to check against the improvement in binary size, with a small amount of translations the effect is minimal so if someone has a project with a pretty big amount of translations and would be ok to share it so I can do some tests I would really appreciate.

@kakserpom
Copy link

@Baptistemontan hey! are you going to continue to work on this?

@Baptistemontan
Copy link
Owner

@kakserpom Yes! I've just have some personal issue to take care of and I'll be back to maintain this library like I used to, I'm sorry for the wait and the lack of update, but I'll be back as soon as I can

@kakserpom
Copy link

kakserpom commented Jun 16, 2024

@Baptistemontan Let me know if Is there anything I can do to help the situation.

Given the uncertain future of leptos-i18n I had to resort to using https://github.com/mondeja/leptos-fluent, however dynamic loading is not even planned there. I am building a real project with i18n to dozens of languages, so static loading is clearly not an option.

@alilee
Copy link

alilee commented Jun 19, 2024

@kakserpom Yes! I've just have some personal issue to take care of and I'll be back to maintain this library like I used to, I'm sorry for the wait and the lack of update, but I'll be back as soon as I can

Don't apologise; yours is great software and we're lucky you're doing it! Thanks!

@alilee
Copy link

alilee commented Jul 28, 2024

It occurs to me that doing this really well needs to consider islands. For server-generated content, you don't need to send the full locale data - just the keys that are necessary for reactive content in the islands. When the user changes locale - regenerate the page mostly from static server-side locale, and then send the smaller dynamic locale for the islands to reference.. make sense?

@Baptistemontan
Copy link
Owner

It does make sense, but that would force user to use islands, and I don't want that. I have some ideas I want to try when I'm done with 0.3, current implementation in #97 hit a major roadblock and I got some vague ideas how to circumvent those, I'll keep this issue updated about the progress

@Baptistemontan Baptistemontan pinned this issue Jul 28, 2024
@Baptistemontan Baptistemontan linked a pull request Jul 28, 2024 that will close this issue
7 tasks
@Baptistemontan Baptistemontan self-assigned this Jul 28, 2024
@Baptistemontan Baptistemontan added the enhancement New feature or request label Jul 28, 2024
@alilee
Copy link

alilee commented Jul 28, 2024

Good point. I think it would work by gating some part of the translation behind an "ssr-only" key so it is not incorporated into the resource struct. Then if there were no ssr-only keys then it would work as for baseline.

@Baptistemontan Baptistemontan mentioned this issue Aug 1, 2024
11 tasks
@Baptistemontan Baptistemontan unpinned this issue Aug 1, 2024
@Baptistemontan Baptistemontan added this to the v0.5 milestone Aug 4, 2024
@Baptistemontan
Copy link
Owner

Baptistemontan commented Aug 15, 2024

I've been thinking about this problem lately and I want to explain what I have in mind. My goals for this feature are:

  • Not bake the translations in the client binary
  • Keep the API close to the old one
  • Reduce code size

The way I see this is to keep the same-ish implementation for the server, bigger binary size but without the overhead of loading translations and the trouble to deal with Sync/Send, and add a leptos server action to retrieve the translations for a given locale/namespace. (Yes, if namespaces are used the loading will be by namespaces instead of by locale). The server will also bake into the sent html file the used locales/namespaces translations that will be parsed at page load. Most i18n framework let you decide what namespace should be sent at what page, I found this pretty tedious and hard to keep in sync. Instead, when a new page is requested to the server, the locales/namespaces that are used are being marked, and all the marked one are sent within the html. That's basically the only added things to the server.

For the client: The translations will be kept inside a thread_local in basically a HashMap<Locale, &'static Translations>. You my be questioning where does the &'static comes from, but when I tried to implement it in #97 one troube was to create the view with borrowed data from the translations. a thread_local is not static, so you can't borrow from it staticly, and it was out of the question to have a TRANSLATIONS.with(..) for every render/access/ect. So I will assume that on the client, there is only one thread, and it's the main one, and when that thread exit the whole application exit. This allows me to do Box::leak without fearing actual memory leak, and can now retrieve a static ref from a thread_local. If this changes in the future then I will be using something like a OnceLock instead of a thread_local. When the application start on the client, the lib will look into the window object to find the translations, parse them and populate a part of the previously explained HashMap, then the page is hydrated. The translations are retrieved on access, this means if you change the locale the only requested translations will be the used one.
The cycle will look like this:
Tries to access the translations, they are not in the thread_local => send a request to the server with the server action => for the time being receive back a leptos Signal to an Option => keep the old translations displayed => when the response comes back parse it, populate the thread_local, trigger the previously created Signal with the translations => render the new translations.

For CSR it' a bit more complicated, this is where I don't know how to do it right, there is no server to request. The translations must be a file that the client can request. So, what's the best solution? make a build.rs function to create a file for each locale/namespace? let the user decide where they are and what path should the request hit ? That's one of the roadblock for #97.

#97 will not be continued: too old to the main branch and lot of details I thought of a better way to do, but I'll be highly inspired by it for v0.5

I'm working on finishing v0.4 because it brings features I find important and add a lot of building blocs that can help with this issue, I want to consolidate the base before adding something this big.

If anybody as a suggestion to make based on anything I just described, or want to no more about how I want to handle a specific problem they think of, please feel free to write a comment and tag me in.

@Baptistemontan Baptistemontan removed a link to a pull request Sep 29, 2024
7 tasks
@Baptistemontan Baptistemontan linked a pull request Sep 29, 2024 that will close this issue
6 tasks
@Baptistemontan
Copy link
Owner

Baptistemontan commented Sep 29, 2024

With leptos 0.7 coming I've been updating the library and updating #133 with it was a nightmare, so I scraped it, and I came into some other difficulties working on #133. With what I learned with #97 and #133 I have now a clear vision on how to implement those things. I've made a working alpha with #139 that works with leptos 0.7, I'm pretty happy of the result and hope it can bring down binary sizes with big translations. I want to add a disclamer: there is a high chance that the code for downloading the translations can be bigger that embedding the translations if you have a few keys, but I'm sure with large enough translations the binary size will shrink. The 3 things I have to work on to make it complete is to fix the "interpolate_display" feature, to create some docs, and to make it work with CSR. Yes for now it only work with SSR as it use server_fn to request the translations. If you use neither "interpolate_display" nor CSR, activating the "dynamic_load" feature should work right out of the box. If some of you can use this branch and try it out I would love some feedbacks, if it's working as intended, if there are issues, some bugs, what binary sizes are looking likes ect...

@Baptistemontan
Copy link
Owner

Just fixed the "interpolate_display" feature, so only thing not working is CSR for now

@Baptistemontan
Copy link
Owner

Baptistemontan commented Oct 12, 2024

Ok so some testing confirmed what I thought, there is a large increase in binary size for very simple apps, as it brings the code for server functions and all that comes with loading the translations, but the cost look fixed, and if you already use server functions the cost is even less. With big enough apps it is totally worth it.

There is no significant binary size changes for static loading (it even shows a size decrease for some examples).

I'll merge the branch but keep this issue opened as it is still not usable for CSR apps, I have an idea on how to implement it for them but it will need to split some crates, so a big refactor again. That split will not just be beneficial for dynamic loading but also for reducing the ICU4X binary footprint (adressed in #144) further.

@Baptistemontan Baptistemontan removed a link to a pull request Oct 12, 2024
6 tasks
@Baptistemontan
Copy link
Owner

Just released v0.5.0-gamma containing the changes for lazy loading

@Baptistemontan
Copy link
Owner

I'm happy to announce that it is now also working with CSR.

It's been a year since this issue was open and it required a massive rewrite to get here, but I'm very happy for this crate to support such feature.

@Baptistemontan
Copy link
Owner

The only missing piece I can think of is to cache the translations in the user localstorage, but I don't know if it is a good idea, so if any of you would give me their point of view on this topic I would be happy to hear it !

@alilee
Copy link

alilee commented Jan 11, 2025

Ideally, would you want your translations static and managed by the CDN and browser caching?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants