Skip to content

Commit

Permalink
Merge pull request #96 from Baptistemontan/island_support
Browse files Browse the repository at this point in the history
Island support
  • Loading branch information
Baptistemontan authored Feb 15, 2024
2 parents 47e26d5 + 3f94764 commit 1d1f319
Show file tree
Hide file tree
Showing 21 changed files with 447 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
strategy:
fail-fast: false
matrix:
examples: [hello_world_actix, hello_world_axum, workspace]
examples: [hello_world_actix, hello_world_axum, workspace, axum_island]
steps:
- name: "Checkout repo"
uses: actions/checkout@v3
Expand Down
38 changes: 38 additions & 0 deletions docs/book/src/usage/02_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,41 @@ pub fn Foo() -> impl IntoView {
If you enable the `nightly` feature you can directly call the context`i18n(new_locale);`.

A non-reactive counterpart to `set_locale` exist: `set_locale_untracked`.

## Note on island

If you use the `experimental-islands` feature from Leptos this will not work and cause an error on the client:

```rust
#[component]
fn App() -> impl IntoView {
provide_i18n_context();

view! {
<HomePage />
}
}

#[island]
fn HomePage() -> impl IntoView {
let i18n = use_i18n();
view! {
<p>{t!(i18n, hello_world)}</p>
}
}
```

Because `App` is only rendered on the server, and the code is never called on the client, thus the context is never provided on the client, making `use_i18n` panic when trying to access it.

To fix it first enable the `experimental-islands` feature for `leptos_i18n` and use the `I18nContextProvider` component exported by the `i18n` module:

```rust
#[component]
fn App() -> impl IntoView {
view! {
<I18nContextProvider>
<HomePage />
</I18nContextProvider>
}
}
```
10 changes: 10 additions & 0 deletions docs/expanded_macros/load_locales.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,16 @@ pub mod i18n {
leptos_i18n::provide_i18n_context()
}

mod provider {
// #[leptos::island] if the `experimental-islands` feature is enabled
#[leptos::component]
pub fn I18nContextProvider(children: leptos::Children) -> impl leptos::IntoView {
super::provide_i18n_context();
children()
}
}
pub use provider::I18nContextProvider; // this is to avoid bloat with the generated struct of the component

// re-export `t!` and `td!` to just need to do `use i18n::*` and basically import everything you need.
pub use leptos_i18n::{t, td};

Expand Down
3 changes: 3 additions & 0 deletions examples/axum_island/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Cargo.lock
target
!.vscode
5 changes: 5 additions & 0 deletions examples/axum_island/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"lokalise.i18n-ally",
]
}
10 changes: 10 additions & 0 deletions examples/axum_island/.vscode/i18n-ally-custom-framework.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
languageIds:
- rust

usageMatchRegex:
- "[^\\w\\d]t!\\(\\s*[\\w.:]*,\\s*([\\w.]*)"
- "[^\\w\\d]td!\\(\\s*[\\w.:]*,\\s*([\\w.]*)"
- "[^\\w\\d]td_string!\\(\\s*[\\w.:]*,\\s*([\\w.]*)"
- "[^\\w\\d]td_display!\\(\\s*[\\w.:]*,\\s*([\\w.]*)"

monopoly: true
4 changes: 4 additions & 0 deletions examples/axum_island/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "locales"
}
107 changes: 107 additions & 0 deletions examples/axum_island/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
[package]
name = "axum_island"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.7", optional = true }
leptos = { version = "0.6", features = ["experimental-islands"] }
leptos_meta = "0.6"
leptos_axum = { version = "0.6", optional = true, features = [
"experimental-islands",
] }
leptos_i18n = { path = "../../leptos_i18n", features = [
"debug_interpolations",
"track_locale_files",
"experimental-islands",
] }
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = { version = "0.1", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
simple_logger = "4"
tokio = { version = "1.35", features = ["rt-multi-thread"], optional = true }
log = "0.4"
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }

[features]
hydrate = [
"dep:console_error_panic_hook",
"dep:wasm-bindgen",
"leptos/hydrate",
"leptos_i18n/hydrate",
"leptos_meta/hydrate",
]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_i18n/axum",
]

[package.metadata.leptos-i18n]
default = "en"
locales = ["en", "fr"]

[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hello_world_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
# style-file = "src/styles/tailwind.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]

# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false

# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]

# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

# Additional files your application could depends on.
# A change to any file in those directories will trigger a rebuild.
#
# Optional.
watch-additional-files = ["locales"]

[package.metadata.cargo-all-features]
skip_feature_sets = [["hydrate", "ssr"]]
17 changes: 17 additions & 0 deletions examples/axum_island/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Hello World Example

This example showcase how to use `leptos_i18n` with experimental `island` feature from Leptos.

## How to run

Simply use `cargo_leptos` to run it:

```sh
cargo leptos watch
```

and to build:

```sh
cargo leptos build --release
```
6 changes: 6 additions & 0 deletions examples/axum_island/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"hello_world": "Hello World!",
"click_to_change_lang": "Click to change language",
"click_count": "You clicked <b>{{ count }}</b> times",
"inc": "Click to increment the counter"
}
6 changes: 6 additions & 0 deletions examples/axum_island/locales/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"hello_world": "Bonjour le monde!",
"click_to_change_lang": "Cliquez pour changez de langue",
"click_count": "Vous avez cliqué <b>{{ count }}</b> fois",
"inc": "Cliquez pour incrémenter le compteur"
}
Binary file added examples/axum_island/public/favicon.ico
Binary file not shown.
54 changes: 54 additions & 0 deletions examples/axum_island/src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::i18n::*;
use leptos::*;

#[component]
pub fn App() -> impl IntoView {
// use the `I18nContextProvider` instead of `provide_i18n_context` to provide the context to all island in the application.
view! {
<I18nContextProvider>
<Counter />
<ChangeLang />
</I18nContextProvider>
}
}

#[island]
fn Counter() -> impl IntoView {
let i18n = use_i18n();

let (counter, set_counter) = create_signal(0);

let inc = move |_| set_counter.update(|count| *count += 1);

let count = move || counter.get();

view! {
<p>
{t!{
i18n,
click_count,
count,
<b> = <b />,
}}
</p>
<button on:click=inc>{t!(i18n, inc)}</button>
}
}

#[island]
fn ChangeLang() -> impl IntoView {
let i18n = use_i18n();

let on_switch = move |_| {
let new_lang = match i18n.get_locale() {
Locale::en => Locale::fr,
Locale::fr => Locale::en,
};
i18n.set_locale(new_lang);
};

view! {
<h1>{t!(i18n, hello_world)}</h1>
<button on:click=on_switch>{t!(i18n, click_to_change_lang)}</button>
}
}
40 changes: 40 additions & 0 deletions examples/axum_island/src/fileserv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::app::App;
use axum::response::Response as AxumResponse;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::IntoResponse,
};
use leptos::*;
use tower::ServiceExt;
use tower_http::services::{ServeDir, fs::ServeFileSystemResponseBody};

pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), App);
handler(req).await.into_response()
}
}

async fn get_static_file(uri: Uri, root: &str) -> Result<Response<ServeFileSystemResponseBody>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
ServeDir::new(root)
.oneshot(req).await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {err}")))
}
13 changes: 13 additions & 0 deletions examples/axum_island/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![deny(warnings)]

pub mod app;
#[cfg(feature = "ssr")]
pub mod fileserv;
leptos_i18n::load_locales!();

#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}
41 changes: 41 additions & 0 deletions examples/axum_island/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#![deny(warnings)]

#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::post, Router};
use axum_island::app::App;
use axum_island::fileserv::file_and_error_handler;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use tokio::net::TcpListener;

simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");

// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);

// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);

// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
logging::log!("listening on http://{}", &addr);
let listener = TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
fn main() {}
Empty file.
Loading

0 comments on commit 1d1f319

Please sign in to comment.