Skip to main content

dioxus_translate/
lib.rs

1use dioxus::prelude::*;
2pub use dioxus_translate_macro::*;
3pub use dioxus_translate_types::Translator;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8static LANGUAGE: GlobalSignal<Language> = Signal::global(|| {
9    #[cfg(target_arch = "wasm32")]
10    {
11        if let Some(lang) = read_local_storage(STORAGE_KEY) {
12            if let Ok(l) = lang.parse::<Language>() {
13                return l;
14            }
15        }
16
17        if let Some(lang) = browser_language() {
18            if let Ok(l) = lang.parse::<Language>() {
19                return l;
20            }
21        }
22    }
23
24    Language::default()
25});
26
27pub const STORAGE_KEY: &str = "language";
28
29pub fn use_translate<T: Translator>() -> T {
30    let lang = use_language();
31    let l = lang();
32
33    translate::<T>(&l)
34}
35
36#[cfg(target_arch = "wasm32")]
37pub fn use_language() -> Signal<Language> {
38    LANGUAGE.signal()
39}
40
41/// On non-wasm targets the same binary runs both the SSR server and native
42/// clients (desktop / mobile). Distinguish them at runtime via
43/// `FullstackContext`: if a fullstack request context exists we're handling
44/// an HTTP render and should read the language from the request cookie; if
45/// not we're a native client and the language lives in the global
46/// `LANGUAGE` signal — the same source of truth as web — hydrated from the
47/// WebView's `localStorage` by the app layer on startup.
48#[cfg(not(target_arch = "wasm32"))]
49pub fn use_language() -> Signal<Language> {
50    use dioxus::fullstack::FullstackContext;
51    if FullstackContext::current().is_some() {
52        use_signal(|| language_from_cookie())
53    } else {
54        LANGUAGE.signal()
55    }
56}
57
58/// Overwrite the global language signal. Used by the app-level persistence
59/// layer to restore a saved preference on startup before any component
60/// renders reading the signal.
61pub fn set_initial_language(lang: Language) {
62    LANGUAGE.signal().set(lang);
63}
64
65/// Reads the language from the `language` cookie in the current HTTP request.
66/// Uses `FullstackContext::current()` to access request headers during SSR.
67/// Returns `Language::default()` if no context or cookie is found.
68#[cfg(not(target_arch = "wasm32"))]
69pub fn language_from_cookie() -> Language {
70    use dioxus::fullstack::FullstackContext;
71
72    let Some(ctx) = FullstackContext::current() else {
73        return Language::default();
74    };
75    let parts = ctx.parts_mut();
76    parts
77        .headers
78        .get("cookie")
79        .and_then(|v| v.to_str().ok())
80        .and_then(|cookies| {
81            cookies
82                .split(';')
83                .find_map(|c| c.trim().strip_prefix("language="))
84        })
85        .and_then(|v| v.parse::<Language>().ok())
86        .unwrap_or_default()
87}
88
89/// Sets the global language signal value.
90pub fn set_language(lang: Language) {
91    LANGUAGE.signal().set(lang);
92
93    #[cfg(target_arch = "wasm32")]
94    {
95        use web_sys::wasm_bindgen::JsCast;
96
97        if let Some(window) = web_sys::window() {
98            if let Ok(Some(storage)) = window.local_storage() {
99                let _ = storage.set_item(STORAGE_KEY, &next.to_string());
100            }
101
102            if let Some(doc) = window.document() {
103                let html_document = doc.dyn_into::<web_sys::HtmlDocument>().unwrap();
104                let _ = html_document.set_cookie(&format!("language={}; path=/;", next));
105            }
106        }
107    }
108}
109
110#[cfg(target_arch = "wasm32")]
111fn read_local_storage(key: &str) -> Option<String> {
112    web_sys::window()?
113        .local_storage()
114        .ok()??
115        .get_item(key)
116        .ok()?
117}
118
119#[cfg(target_arch = "wasm32")]
120fn browser_language() -> Option<String> {
121    let lang = web_sys::window()?.navigator().language()?;
122    Some(lang.split('-').next().unwrap_or(&lang).to_string())
123}
124
125pub fn translate<T: Translator>(lang: &Language) -> T {
126    match lang {
127        #[cfg(feature = "ko")]
128        Language::Ko => T::ko(),
129        Language::En => T::en(),
130    }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy, JsonSchema)]
134pub enum Language {
135    #[cfg(feature = "ko")]
136    #[serde(rename = "ko")]
137    Ko,
138    #[serde(rename = "en")]
139    En,
140}
141
142impl Default for Language {
143    fn default() -> Self {
144        Language::En
145    }
146}
147
148impl Language {
149    pub fn switch(&self) -> Self {
150        #[cfg(feature = "ko")]
151        let next = match self {
152            Language::Ko => Language::En,
153            Language::En => Language::Ko,
154        };
155
156        #[cfg(not(feature = "ko"))]
157        let next = Language::En;
158
159        set_language(next);
160
161        next
162    }
163
164    pub fn open_graph_locale(&self) -> String {
165        match self {
166            #[cfg(feature = "ko")]
167            Language::Ko => "ko_KR".to_string(),
168            Language::En => "en_US".to_string(),
169        }
170    }
171}
172
173impl std::fmt::Display for Language {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        match self {
176            #[cfg(feature = "ko")]
177            Language::Ko => write!(f, "ko"),
178            Language::En => write!(f, "en"),
179        }
180    }
181}
182
183impl std::str::FromStr for Language {
184    type Err = String;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        match s {
188            #[cfg(feature = "ko")]
189            "ko" => Ok(Language::Ko),
190            "en" => Ok(Language::En),
191            _ => Ok(Language::En),
192        }
193    }
194}
195
196impl Language {
197    pub fn to_string(&self) -> String {
198        match self {
199            #[cfg(feature = "ko")]
200            Language::Ko => "ko".to_string(),
201            Language::En => "en".to_string(),
202        }
203    }
204
205    pub fn all() -> Vec<Language> {
206        #[cfg(feature = "ko")]
207        {
208            vec![Language::Ko, Language::En]
209        }
210        #[cfg(not(feature = "ko"))]
211        {
212            vec![Language::En]
213        }
214    }
215}
216
217pub trait Translate {
218    fn translate(&self, lang: &Language) -> &'static str;
219}