aboutsummaryrefslogblamecommitdiffstats
path: root/src/i18n.rs
blob: 751a0eacd66a6048bf59e23bd9a3681d345b117c (plain) (tree)






















                                                                                              































































































                                                                                            
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<E: Endpoint>(next: E, mut req: Request) -> poem::Result<E::Output> {
    if let Ok(params) = req.params::<Params>() {
        let session = req
            .extensions()
            .get::<Session>()
            .expect("To use the `set_language` middleware, the `Session` data is required.");
        session.set("lang", params.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<str>,
        args: impl Into<I18NArgs<'a>>,
    ) -> Result<String, I18NError> {
        self.0.text_with_args(id, args)
    }

    /// Gets the text.
    ///
    /// See also: [`I18NBundle::text`](I18NBundle::text)
    pub fn text(&self, id: impl AsRef<str>) -> Result<String, I18NError> {
        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<Self> {
        let session = req
            .extensions()
            .get::<Session>()
            .expect("To use the `Locale` extractor, the `Session` data is required.");
        let resources = req
            .extensions()
            .get::<I18NResources>()
            .expect("To use the `Locale` extractor, the `I18NResources` data is required.");

        let mut lang_id = None;
        if let Some(lang) = session.get::<String>("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<LanguageIdentifier> {
    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<u16> {
    let mut parts = value.split('=');
    let name = parts.next()?.trim();
    if name != "q" {
        return None;
    }
    let q = parts.next()?.trim().parse::<f32>().ok()?;
    Some((q.clamp(0.0, 1.0) * 1000.0) as u16)
}