#![doc = include_str!("../DIOXUS.md")]
use crate::config::{I18n, I18nConfig, StorageType};
use dioxus::prelude::*;
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
use web_sys::{Storage, wasm_bindgen::JsCast, window};
#[derive(Props, PartialEq, Clone)]
pub struct I18nProviderProps {
#[props(default)]
pub translations: HashMap<&'static str, &'static str>,
pub children: Element,
#[props(default)]
pub storage_type: StorageType,
#[props(default = "i18nrs".to_string())]
pub storage_name: String,
#[props(default = "en".to_string())]
pub default_language: String,
#[props(default)]
pub onchange: EventHandler<String>,
#[props(default)]
pub onerror: EventHandler<String>,
}
#[derive(Clone)]
pub struct I18nContext {
pub i18n: Signal<I18n>,
pub set_language: EventHandler<String>,
}
#[component]
pub fn I18nProvider(props: I18nProviderProps) -> Element {
let initial_language =
use_initial_language(props.storage_type.clone(), props.storage_name.clone())()
.unwrap_or(props.default_language.clone());
#[cfg(target_arch = "wasm32")]
fn is_rtl_language(lang: &str) -> bool {
matches!(lang, "ar" | "he" | "fa" | "ur" | "ps" | "ku" | "sd")
}
let update_text_direction = |_lang: &str| {
#[cfg(target_arch = "wasm32")]
{
if let Some(document) = window().and_then(|win| win.document()) {
let dir = if is_rtl_language(_lang) { "rtl" } else { "ltr" };
if let Some(html_element) = document.document_element() {
let _ = html_element.set_attribute("dir", dir);
}
}
}
};
update_text_direction(&initial_language.clone());
let mut i18n = use_signal(|| {
I18n::new(
I18nConfig {
translations: props.translations.clone(),
},
props.translations.clone(),
)
.map(|mut instance| {
if let Err(err) = instance.set_translation_language(
&initial_language.clone(),
&props.storage_type,
&props.storage_name,
) {
props.onerror.call(err.clone());
}
instance
})
.unwrap_or_else(|err| {
props.onerror.call(err.clone());
panic!("Failed to initialize I18n: {}", err);
})
});
let set_language = EventHandler::new({
move |language: String| {
let mut i18n_val = i18n();
update_text_direction(&language);
let lang = language.clone();
if i18n_val
.set_translation_language(&language, &props.storage_type, &props.storage_name)
.is_ok()
{
i18n.set(i18n_val);
props.onchange.call(language);
let storage_name = props.storage_name.clone();
#[cfg(feature = "dio-ssr")]
spawn(async move {
let lang = lang.clone();
let _ = set_cookie(storage_name, lang).await;
});
}
}
});
let context = I18nContext { i18n, set_language };
provide_context(context);
rsx! { {props.children} }
}
pub fn use_i18n() -> I18nContext {
consume_context::<I18nContext>()
}
#[allow(unused)]
pub fn use_initial_language(storage_type: StorageType, key: String) -> Signal<Option<String>> {
let mut language = use_signal(|| None);
#[cfg(target_arch = "wasm32")]
{
let stored: Option<String> = match storage_type {
StorageType::LocalStorage => window()
.expect("No window object")
.local_storage()
.expect("Failed to access localStorage")
.and_then(|s| s.get_item(&key).ok())
.expect("Stored language not found in localStorage"),
StorageType::SessionStorage => window()
.expect("No window object")
.session_storage()
.expect("Failed to access sessionStorage")
.and_then(|s| s.get_item(&key).ok())
.expect("Stored language not found in sessionStorage"),
};
language.set(stored);
#[cfg(feature = "dio-ssr")]
let cookie = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.dyn_into::<web_sys::HtmlDocument>().ok())
.and_then(|html_doc| html_doc.cookie().ok())
.and_then(|c| {
c.split(';')
.map(|c| c.trim())
.find_map(|c| c.strip_prefix(&format!("{key}=")))
.map(|v| v.to_owned())
});
#[cfg(feature = "dio-ssr")]
language.set(cookie);
#[cfg(feature = "dio-ssr")]
spawn(async move {
let key = key.clone();
let _cookie = get_cookie(key).await.unwrap();
});
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "dio-ssr")]
{
server_only! {
use_future({
use crate::dioxus::dioxus_fullstack::FullstackContext;
use http::header::{COOKIE, SET_COOKIE};
use http::HeaderValue;
let key = key.to_owned();
move || {let value = key.clone();
async move {
let value = value.clone();
let ctx_opt = FullstackContext::current();
let ctx = match ctx_opt {
Some(c) => c,
None => return,
};
let parts_guard = ctx.parts_mut();
let headers = &parts_guard.headers;
if let Some(raw) = headers
.get(COOKIE)
.and_then(|v: &http::HeaderValue| v.to_str().ok())
&& let Some(v) = raw
.split(';')
.map(|c: &str| c.trim())
.find_map(|c: &str| c.strip_prefix(&format!("{value}=")))
{
language.set(Some(v.to_string()));
return;
}
if let Some(al) = headers
.get("accept-language")
.and_then(|v: &http::HeaderValue| v.to_str().ok())
{
let v = al.split(',').next().unwrap_or("en").trim().to_owned();
language.set(Some(v.clone()));
if let Ok(cookie_val) = HeaderValue::from_str(
&format!("{value}={v}; Path=/; Max-Age=31536000; SameSite=Lax")
) {
ctx.add_response_header(SET_COOKIE, cookie_val);
}
}
}
}
});
}
}
language
}
#[cfg(feature = "dio-ssr")]
#[server]
pub async fn set_cookie(key: String, lang: String) -> Result<(), ServerFnError> {
use crate::dioxus::dioxus_fullstack::FullstackContext;
use http::HeaderValue;
use http::header::SET_COOKIE;
let ctx = FullstackContext::current().ok_or_else(|| ServerFnError::ServerError {
message: "FullstackContext not available".into(),
code: 500,
details: None,
})?;
let value = HeaderValue::from_str(&format!(
"{key}={lang}; Path=/; SameSite=Lax; Max-Age=31536000"
))
.map_err(|e| ServerFnError::ServerError {
message: e.to_string(),
code: 500,
details: None,
})?;
ctx.add_response_header(SET_COOKIE, value);
Ok(())
}
#[cfg(feature = "dio-ssr")]
#[server]
pub async fn get_cookie(key: String) -> Result<String, ServerFnError> {
use crate::dioxus::dioxus_fullstack::FullstackContext;
use http::header::COOKIE;
let ctx = FullstackContext::current().ok_or_else(|| ServerFnError::ServerError {
message: "FullstackContext not available".into(),
code: 500,
details: None,
})?;
let parts_guard = ctx.parts_mut();
let headers = &parts_guard.headers;
if let Some(raw) = headers.get(COOKIE).and_then(|v| v.to_str().ok())
&& let Some(v) = raw
.split(';')
.map(str::trim)
.find_map(|c| c.strip_prefix(&format!("{key}=")))
{
return Ok(v.to_string());
}
Ok("en".to_string())
}