diff --git a/Cargo.lock b/Cargo.lock index a1c2011..b12ddda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "accept-language" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2bc21ffc9b77e9c31e733bb7e937c11dcf6157bb74f80bf94734110aa9b9ebc" + [[package]] name = "adler" version = "1.0.2" @@ -3104,6 +3110,7 @@ dependencies = [ name = "tegakituesday" version = "0.1.0" dependencies = [ + "accept-language", "chrono", "comrak", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 9d5d342..1695f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +accept-language = "2.0.0" chrono = { version = "0.4.26", features = ["serde"] } comrak = "0.18.0" derive_more = "0.99.17" diff --git a/i18n/en.po b/i18n/en.po index e07a00d..02bd6b1 100644 --- a/i18n/en.po +++ b/i18n/en.po @@ -1,2 +1,5 @@ +msgid "lang" +msgstr "English" + msgid "title" msgstr "Tegaki Tuesday" diff --git a/i18n/ja.po b/i18n/ja.po index b1ef571..91a9a53 100644 --- a/i18n/ja.po +++ b/i18n/ja.po @@ -1,2 +1,5 @@ +msgid "lang" +msgstr "日本語" + msgid "title" msgstr "手書きの火曜日" diff --git a/src/cookies.rs b/src/cookies.rs index 239ed95..0e72b9d 100644 --- a/src/cookies.rs +++ b/src/cookies.rs @@ -8,3 +8,4 @@ pub mod user { pub const USER_DISCRIMINATOR_COOKIE: &str = "user_discriminator"; pub const USER_AVATAR_COOKIE: &str = "user_avatar"; } +pub const LANG_COOKIE: &str = "lang"; \ No newline at end of file diff --git a/src/i18n.rs b/src/i18n.rs index 43c5e99..c2cfece 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -1,5 +1,6 @@ use derive_more::From; use rocket_dyn_templates::tera::{self, Value}; +use serde::Serialize; use std::{ collections::HashMap, fs::{self, File}, @@ -16,9 +17,11 @@ pub enum LoadCatalogsError { MissingDefaultLanguage, } -const DEFAULT: &str = "ja"; +pub const DEFAULT: &str = "ja"; -pub fn load_catalogs() -> Result, LoadCatalogsError> { +type Catalogs = HashMap; + +pub fn load_catalogs() -> Result<(Catalogs, Vec), LoadCatalogsError> { let mut catalogs = HashMap::new(); for file in fs::read_dir("i18n")? { let file = file?; @@ -51,19 +54,58 @@ pub fn load_catalogs() -> Result, LoadCatalogsError> { if !catalogs.contains_key(DEFAULT) { return Err(LoadCatalogsError::MissingDefaultLanguage); } - Ok(catalogs) + let langs = catalogs + .iter() + .map(|(code, catalog)| + LangCode { + code: code.clone(), + name: catalog + .gettext("lang") + .to_owned(), + } + ).collect(); + Ok((catalogs, langs)) } pub fn i18n_filter( value: &Value, - _args: &HashMap, - catalogs: &HashMap, + args: &HashMap, + catalogs: &Catalogs, ) -> tera::Result { let key = value .as_str() .ok_or_else(|| tera::Error::msg("The translation key must be a string"))?; - let translation = catalogs.get(DEFAULT).expect("Missing catalog").gettext(key); + let langs = args + .get("lang") + .map(|value| value.as_array()) + .flatten() + .map(|array| { + let mut langs = Vec::with_capacity(array.len()); + for lang in array { + langs.push(lang.as_str().unwrap()); + } + langs + }) + .unwrap_or_else(|| vec![DEFAULT]); + for lang in langs { + if let Some(catalog) = catalogs.get(lang) { + return Ok(Value::String(catalog.gettext(key).to_owned())); + } + } + panic!("Missing catalog"); +} - Ok(Value::String(translation.to_owned())) +#[derive(Serialize)] +pub struct LangCode { + pub code: String, + pub name: String, } + +pub fn langs_filter( + _value: &Value, + _args: &HashMap, + langs: &Vec, +) -> tera::Result { + Ok(serde_json::to_value(langs).unwrap()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b547586..dec2b2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ extern crate rocket; use poise::serenity_prelude::Http; -use rocket::fs::{relative, FileServer}; +use rocket::{fs::{relative, FileServer}}; use rocket_dyn_templates::{tera, Template}; use sass_rocket_fairing::SassFairing; use std::{collections::HashMap, env}; @@ -20,6 +20,8 @@ use routes::*; mod i18n; use i18n::{i18n_filter, load_catalogs}; +use crate::i18n::langs_filter; + mod prelude; #[launch] @@ -43,14 +45,20 @@ async fn rocket() -> _ { routes![get_challenge, login, post_login, success, logout, testing], ) .mount("/css", FileServer::from(relative!("styles/css"))) - .attach(Template::custom(|engines| { + .attach(Template::custom(move |engines| { use tera::Value; - let catalogs = load_catalogs().unwrap(); + let (catalogs, langs) = load_catalogs().unwrap(); engines.tera.register_filter( "i18n", move |value: &Value, args: &HashMap| { i18n_filter(value, args, &catalogs) }, + ); + engines.tera.register_filter( + "langs", + move |value: &Value, args: &HashMap| { + langs_filter(value, args, &langs) + } ) })) .attach(SassFairing::default()) diff --git a/src/routes/get_challenge.rs b/src/routes/get_challenge.rs index 565a51e..dadfb61 100644 --- a/src/routes/get_challenge.rs +++ b/src/routes/get_challenge.rs @@ -3,19 +3,25 @@ use std::ops::Deref; use rocket::{http::CookieJar, State}; use rocket_dyn_templates::{context, Template}; -use crate::models::{Challenge, Settings, User}; +use crate::{models::{Challenge, Settings, User}, cookies::LANG_COOKIE, i18n::DEFAULT as DEFAULT_LANG, utils::AcceptLanguage}; #[get("/")] pub async fn get_challenge( challenge: u32, cookies: &CookieJar<'_>, settings: &State, + accept_language: AcceptLanguage, ) -> Template { Template::render( "index", context! { challenge, settings: settings.deref(), + lang: cookies + .get(LANG_COOKIE) + .map(|cookie| vec![cookie.value().to_owned()]) + .or_else(|| accept_language.0) + .unwrap_or_else(|| vec![DEFAULT_LANG.to_owned()]), user: User::get(cookies).await.unwrap(), content: Challenge::get(challenge), }, diff --git a/src/utils/headers.rs b/src/utils/headers.rs new file mode 100644 index 0000000..2290bb4 --- /dev/null +++ b/src/utils/headers.rs @@ -0,0 +1,30 @@ +use std::convert::Infallible; + +use rocket::{ + request::{self, FromRequest}, + Request, +}; + +macro_rules! header { + ($name:ident, $header:expr) => { + header!($name, $header, String, |header| header.to_owned()); + }; + ($name:ident, $header:expr, $type:ty, $handler:expr) => { + pub struct $name(pub Option<$type>); + + #[rocket::async_trait] + impl<'r> FromRequest<'r> for $name { + type Error = Infallible; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let header = req.headers().get_one($header); + request::Outcome::Success(Self(header.map($handler))) + } + } + }; +} + +header!(Referer, "Referer"); +header!(AcceptLanguage, "Accept-Language", Vec, |header| { + accept_language::parse(header) +}); \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 102353a..d39ec93 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,5 @@ mod kyujitai; pub use kyujitai::Kyujitai; -mod referer; -pub use referer::Referer; +mod headers; +pub use headers::*; diff --git a/src/utils/referer.rs b/src/utils/referer.rs deleted file mode 100644 index 60c9cf1..0000000 --- a/src/utils/referer.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::convert::Infallible; - -use rocket::{ - request::{self, FromRequest}, - Request, -}; - -pub struct Referer(pub Option); - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Referer { - type Error = Infallible; - - async fn from_request(req: &'r Request<'_>) -> request::Outcome { - let referer = req.headers().get_one("Referer"); - request::Outcome::Success(Referer(referer.map(|referer| referer.to_owned()))) - } -} diff --git a/styles/sass/style.scss b/styles/sass/style.scss index 2705cc8..3968357 100644 --- a/styles/sass/style.scss +++ b/styles/sass/style.scss @@ -65,8 +65,8 @@ input[type=number] { flex-direction: column; position: absolute; top: 100%; - left: 0; - width: 100%; + right: 0; + min-width: 100%; } &:not(:hover) .dropdown-content { @@ -74,7 +74,7 @@ input[type=number] { } } - & > *, .dropdown > a { + & > *, .dropdown .link { padding: 0.45em; & > * { @@ -82,7 +82,7 @@ input[type=number] { } } - a { + a, .link { color: var(--fg); font-weight: bold; } @@ -98,7 +98,7 @@ input[type=number] { } } -.dropdown:hover > a, nav > a:hover { +.dropdown:hover > .link, nav > .link:hover { border: none; --fg: #{$fg}; --bg: #{$bg}; diff --git a/templates/index.html.tera b/templates/index.html.tera index 46ffbb2..3e40889 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -21,7 +21,7 @@ {% endif %} {% if user %} - {% else%} Log in {% endif %} +