use crate::{Lang, Phrasebook, Phrases};
pub const DEFAULT_KEY: &str = "lang";
pub fn remembered(book: &Phrasebook, key: &str) -> Option<Lang> {
supported(book, load(key).as_deref())
}
pub fn preferred() -> Vec<String> {
imp::preferred()
}
pub fn initial(book: &Phrasebook) -> Phrases {
initial_with_key(book, DEFAULT_KEY)
}
pub fn initial_with_key(book: &Phrasebook, key: &str) -> Phrases {
let phrases = book.phrases(initial_lang(book, key));
set_document_lang(phrases.lang());
phrases
}
pub fn initial_lang(book: &Phrasebook, key: &str) -> Lang {
choose(book, load(key).as_deref(), &preferred())
}
pub fn set_document_lang(lang: Lang) {
imp::set_document_lang(&lang.to_string());
}
fn supported(book: &Phrasebook, tag: Option<&str>) -> Option<Lang> {
let lang = Lang::parse(tag?)?;
book.supports(lang).then_some(lang)
}
fn choose(book: &Phrasebook, remembered: Option<&str>, preferred: &[String]) -> Lang {
supported(book, remembered)
.unwrap_or_else(|| book.negotiate(preferred.iter().map(String::as_str)))
}
fn load(key: &str) -> Option<String> {
imp::load(key)
}
pub(crate) fn store(lang: Lang, key: &str) {
imp::store(&lang.to_string(), key);
}
#[cfg(target_arch = "wasm32")]
mod imp {
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok().flatten()
}
pub(super) fn load(key: &str) -> Option<String> {
storage()?.get_item(key).ok().flatten()
}
pub(super) fn store(value: &str, key: &str) {
if let Some(storage) = storage() {
let _ = storage.set_item(key, value);
}
}
pub(super) fn set_document_lang(tag: &str) {
if let Some(root) = web_sys::window()
.and_then(|window| window.document())
.and_then(|document| document.document_element())
{
let _ = root.set_attribute("lang", tag);
}
}
pub(super) fn preferred() -> Vec<String> {
let Some(window) = web_sys::window() else {
return Vec::new();
};
let navigator = window.navigator();
let mut out = navigator
.languages()
.iter()
.filter_map(|value| value.as_string())
.collect::<Vec<_>>();
if out.is_empty()
&& let Some(language) = navigator.language()
{
out.push(language);
}
out
}
}
#[cfg(not(target_arch = "wasm32"))]
mod imp {
pub(super) fn load(_key: &str) -> Option<String> {
None
}
pub(super) fn store(_value: &str, _key: &str) {}
pub(super) fn set_document_lang(_tag: &str) {}
pub(super) fn preferred() -> Vec<String> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn book() -> Phrasebook {
Phrasebook::builder()
.language(Lang::EN_US, "English")
.language(Lang::new("sv-SE"), "Svenska")
.build()
}
fn strings(tags: &[&str]) -> Vec<String> {
tags.iter().map(|s| s.to_string()).collect()
}
#[test]
fn remembered_choice_wins() {
let book = book();
let lang = choose(&book, Some("sv-SE"), &strings(&["en-US"]));
assert_eq!(lang, Lang::new("sv-SE"));
}
#[test]
fn unsupported_or_garbled_choice_is_ignored() {
let book = book();
assert_eq!(
choose(&book, Some("de-DE"), &strings(&["sv"])),
Lang::new("sv-SE")
);
assert_eq!(
choose(&book, Some("???"), &strings(&["sv"])),
Lang::new("sv-SE")
);
}
#[test]
fn preferences_are_negotiated() {
let book = book();
assert_eq!(
choose(&book, None, &strings(&["de", "sv"])),
Lang::new("sv-SE")
);
assert_eq!(choose(&book, None, &strings(&["en-GB"])), Lang::EN_US);
assert_eq!(
choose(&book, None, &strings(&["sv_FI", "en"])),
Lang::new("sv-SE")
);
assert_eq!(choose(&book, None, &[]), Lang::EN_US);
}
#[test]
fn host_finds_and_stores_nothing() {
let book = book();
store(Lang::new("sv-SE"), DEFAULT_KEY);
assert_eq!(remembered(&book, DEFAULT_KEY), None);
assert!(preferred().is_empty());
assert_eq!(initial_lang(&book, DEFAULT_KEY), Lang::EN_US);
assert_eq!(initial(&book).lang(), Lang::EN_US);
assert_eq!(initial_with_key(&book, "other").lang(), Lang::EN_US);
book.phrases(Lang::new("sv-SE")).store();
}
}