use tablo_ui::{dropdown_menu_item, dropdown_menu_label, dropdown_menu_separator};
use topcoat::{
context::{Cx, try_context},
cookie::{Cookie, CookieJarCell, Cookies, SameSite, cookies},
icon::icon,
l10n::Locale,
router::{Body, RouteFuture, response::IntoResponse},
view::{BoxView, ViewExt, attributes, view},
};
use crate::{Text, t};
pub(crate) const LANGUAGE_SEGMENT: &str = "locale";
const LOCALE_FIELD: &str = "locale";
const REMEMBERED_DAYS: i64 = 365;
fn language_url(cx: &Cx) -> String {
format!("{}/{LANGUAGE_SEGMENT}", crate::auth::panel_prefix(cx))
}
pub(crate) fn language_post(cx: &Cx, body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let parts = crate::auth::input::read_form(cx, body).await?;
crate::csrf::verify(cx, parts.last(crate::csrf::FIELD_NAME))?;
let picked = parts
.last(LOCALE_FIELD)
.and_then(|tag| crate::i18n::offered_match(cx, tag))
.ok_or_else(topcoat::router::error::forbidden)?;
remember(cx, &picked.to_string());
store(cx, &picked).await;
let target = parts
.last(crate::auth::NEXT_FIELD)
.and_then(crate::auth::safe_next)
.map_or_else(|| crate::auth::panel_prefix(cx), str::to_string);
topcoat::router::error::see_other(target).into_response(cx)
})
}
fn remember(cx: &Cx, tag: &str) {
if try_context::<CookieJarCell>(cx).is_none() {
return;
}
let cookie = Cookie::build((crate::i18n::LOCALE_COOKIE, tag.to_string()))
.path(crate::auth::panel_prefix(cx))
.http_only(true)
.secure(true)
.same_site(SameSite::Lax)
.max_age(topcoat::cookie::time::Duration::days(REMEMBERED_DAYS))
.build();
cookies(cx).add(cookie);
}
async fn store(cx: &Cx, picked: &Locale) {
let Some(signed) = crate::auth::signed(cx) else {
return;
};
if signed.impersonator.is_some() || signed.user.locale().as_ref() == Some(picked) {
return;
}
let Some(authenticator) = signed.realm.auth.authenticator() else {
return;
};
if let Err(error) = authenticator
.store_locale(cx, signed.user.as_ref(), picked)
.await
{
tracing::error!(error = %error, locale = %picked, "the picked locale not stored");
}
}
pub(crate) fn language_choices<'a>(cx: &'a Cx, csrf: &str) -> BoxView<'a> {
let offered = crate::i18n::offered(cx);
if offered.len() < 2 {
return ().boxed();
}
let current = topcoat::l10n::try_locale(cx).cloned();
let action = language_url(cx);
let here = topcoat::router::request::uri(cx)
.path_and_query()
.map_or_else(String::new, ToString::to_string);
let next = crate::auth::safe_next(&here)
.unwrap_or_default()
.to_string();
let choices: Vec<BoxView<'a>> = offered
.iter()
.map(|locale| {
let tag = locale.to_string();
let hook = tag.clone();
let checked = current.as_ref() == Some(locale);
let label =
Text::derived(format!("tablo-shell-language.{tag}"), tag.clone()).localize(cx);
let csrf = crate::csrf::field(cx, csrf);
let action = action.clone();
let next = next.clone();
view! {
<form method="post" action=(action)>
(csrf)
<input type="hidden" name=(crate::auth::NEXT_FIELD) value=(next)>
<input type="hidden" name=(LOCALE_FIELD) value=(tag)>
dropdown_menu_item(
attrs: attributes! {
type="submit"
data-locale=(hook)
aria-current=(checked.then_some("true"))
},
<span class="flex-1 truncate">(label)</span>
if checked {
icon(data: tablo_ui::icons::CHECK)
}
)
</form>
}
.boxed()
})
.collect();
view! {
dropdown_menu_separator()
dropdown_menu_label((t!(cx, "tablo-shell-language")))
for choice in choices {
(choice)
}
}
.boxed()
}