From 2b8623fde242d379c66e4c532f7cad0dbb2198aa Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Mon, 12 Feb 2024 01:39:00 +0000 Subject: add language select widget --- src/i18n.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 17 +++++++- src/templates.rs | 2 +- 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/i18n.rs (limited to 'src') diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..964aaa3 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,120 @@ +use std::str::FromStr; + +use poem::{ + error::I18NError, + http::header, + i18n::{unic_langid::LanguageIdentifier, I18NArgs, I18NResources}, + session::Session, + Endpoint, FromRequest, Middleware, Request, RequestBody, +}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Params { + lang: String, +} + +pub async fn set_language(next: E, mut req: Request) -> poem::Result { + if let Ok(params) = req.params::() { + let session = req + .extensions() + .get::() + .expect("To use the `set_language` middleware, the `Session` data is required."); + session.set("lang", params.lang); + println!("{:?}", session.get::("lang")) + } + next.call(req).await +} + +pub struct Locale(poem::i18n::I18NBundle); + +impl Locale { + /// Gets the text with arguments. + /// + /// See also: [`I18NBundle::text_with_args`](I18NBundle::text_with_args) + pub fn text_with_args<'a>( + &self, + id: impl AsRef, + args: impl Into>, + ) -> Result { + self.0.text_with_args(id, args) + } + + /// Gets the text. + /// + /// See also: [`I18NBundle::text`](I18NBundle::text) + pub fn text(&self, id: impl AsRef) -> Result { + self.0.text(id) + } +} + +#[poem::async_trait] +impl<'a> FromRequest<'a> for Locale { + async fn from_request(req: &'a Request, body: &mut RequestBody) -> poem::Result { + let session = req + .extensions() + .get::() + .expect("To use the `Locale` extractor, the `Session` data is required."); + let resources = req + .extensions() + .get::() + .expect("To use the `Locale` extractor, the `I18NResources` data is required."); + + let mut lang_id = None; + if let Some(lang) = session.get::("lang") { + lang_id = Some(lang); + }; + + if let Some(lang_id) = lang_id { + if let Ok(lang_id) = LanguageIdentifier::from_str(&lang_id) { + return Ok(Self(resources.negotiate_languages(&[&lang_id]))); + } + }; + + let accept_languages = req + .headers() + .get(header::ACCEPT_LANGUAGE) + .and_then(|value| value.to_str().ok()) + .map(parse_accept_languages) + .unwrap_or_default(); + + Ok(Self(resources.negotiate_languages(&accept_languages))) + } +} + +fn parse_accept_languages(value: &str) -> Vec { + let mut languages = Vec::new(); + + for s in value.split(',').map(str::trim) { + if let Some(res) = parse_language(s) { + languages.push(res); + } + } + + languages.sort_by(|(_, a), (_, b)| b.cmp(a)); + languages + .into_iter() + .map(|(language, _)| language) + .collect() +} + +fn parse_language(value: &str) -> Option<(LanguageIdentifier, u16)> { + let mut parts = value.split(';'); + let name = parts.next()?.trim(); + let quality = match parts.next() { + Some(quality) => parse_quality(quality).unwrap_or_default(), + None => 1000, + }; + let language = LanguageIdentifier::from_str(name).ok()?; + Some((language, quality)) +} + +fn parse_quality(value: &str) -> Option { + let mut parts = value.split('='); + let name = parts.next()?.trim(); + if name != "q" { + return None; + } + let q = parts.next()?.trim().parse::().ok()?; + Some((q.clamp(0.0, 1.0) * 1000.0) as u16) +} diff --git a/src/main.rs b/src/main.rs index d810159..8e31fee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod article; mod atom; mod blog; mod error; +mod i18n; mod live; mod poetry; mod posts; @@ -14,9 +15,12 @@ mod utils; use std::{collections::HashSet, time::Duration}; +use i18n::set_language; use poem::http::StatusCode; use poem::i18n::unic_langid::langid; -use poem::i18n::{I18NResources, Locale}; +use poem::i18n::I18NResources; +use poem::session::{CookieConfig, CookieSession, ServerSession}; +use poem::web::cookie::{CookieKey, SameSite}; use poem::{ endpoint::EmbeddedFilesEndpoint, get, handler, @@ -36,6 +40,7 @@ use tracing_subscriber::FmtSubscriber; use crate::article::Article; use crate::blog::Blogpost; +use crate::i18n::Locale; use crate::poetry::Poem; use crate::posts::Post; @@ -208,6 +213,14 @@ async fn main() -> std::result::Result<(), std::io::Error> { .build() .unwrap(); + let session = CookieSession::new( + // CookieConfig::private(CookieKey::generate()) + // .name("blossom") + // .domain("blos.sm") + // .same_site(SameSite::Strict), + CookieConfig::default(), + ); + let blossom = Route::new() .at("/", get(home)) .at("/blog", get(get_blog)) @@ -220,7 +233,9 @@ async fn main() -> std::result::Result<(), std::io::Error> { .nest("/static/", EmbeddedFilesEndpoint::::new()) .catch_all_error(custom_error) .data(resources) + .around(set_language) .with(Tracing) + .with(session) .with(AddData::new( reqwest::Client::builder() .connect_timeout(Duration::from_secs(1)) diff --git a/src/templates.rs b/src/templates.rs index 2ab9c41..66462eb 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use askama::Template; use poem::http::StatusCode; -use poem::i18n::Locale; use rand::{thread_rng, Rng}; +use crate::i18n::Locale; use crate::poetry; use crate::posts::Post; use crate::{blog, scrobbles::NowPlayingData}; -- cgit