Working localization
This commit is contained in:
parent
245fcbcf1e
commit
3abc45cd4f
14 changed files with 135 additions and 40 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
msgid "lang"
|
||||
msgstr "English"
|
||||
|
||||
msgid "title"
|
||||
msgstr "Tegaki Tuesday"
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
msgid "lang"
|
||||
msgstr "日本語"
|
||||
|
||||
msgid "title"
|
||||
msgstr "手書きの火曜日"
|
||||
|
|
|
@ -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";
|
58
src/i18n.rs
58
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<HashMap<String, Catalog>, LoadCatalogsError> {
|
||||
type Catalogs = HashMap<String, Catalog>;
|
||||
|
||||
pub fn load_catalogs() -> Result<(Catalogs, Vec<LangCode>), 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<HashMap<String, Catalog>, 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<String, Value>,
|
||||
catalogs: &HashMap<String, Catalog>,
|
||||
args: &HashMap<String, Value>,
|
||||
catalogs: &Catalogs,
|
||||
) -> tera::Result<Value> {
|
||||
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);
|
||||
|
||||
Ok(Value::String(translation.to_owned()))
|
||||
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");
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LangCode {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub fn langs_filter(
|
||||
_value: &Value,
|
||||
_args: &HashMap<String, Value>,
|
||||
langs: &Vec<LangCode>,
|
||||
) -> tera::Result<Value> {
|
||||
Ok(serde_json::to_value(langs).unwrap())
|
||||
}
|
14
src/main.rs
14
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<String, Value>| {
|
||||
i18n_filter(value, args, &catalogs)
|
||||
},
|
||||
);
|
||||
engines.tera.register_filter(
|
||||
"langs",
|
||||
move |value: &Value, args: &HashMap<String, Value>| {
|
||||
langs_filter(value, args, &langs)
|
||||
}
|
||||
)
|
||||
}))
|
||||
.attach(SassFairing::default())
|
||||
|
|
|
@ -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("/<challenge>")]
|
||||
pub async fn get_challenge(
|
||||
challenge: u32,
|
||||
cookies: &CookieJar<'_>,
|
||||
settings: &State<Settings>,
|
||||
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),
|
||||
},
|
||||
|
|
30
src/utils/headers.rs
Normal file
30
src/utils/headers.rs
Normal file
|
@ -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<Self, Self::Error> {
|
||||
let header = req.headers().get_one($header);
|
||||
request::Outcome::Success(Self(header.map($handler)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
header!(Referer, "Referer");
|
||||
header!(AcceptLanguage, "Accept-Language", Vec<String>, |header| {
|
||||
accept_language::parse(header)
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
mod kyujitai;
|
||||
pub use kyujitai::Kyujitai;
|
||||
|
||||
mod referer;
|
||||
pub use referer::Referer;
|
||||
mod headers;
|
||||
pub use headers::*;
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use rocket::{
|
||||
request::{self, FromRequest},
|
||||
Request,
|
||||
};
|
||||
|
||||
pub struct Referer(pub Option<String>);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Referer {
|
||||
type Error = Infallible;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let referer = req.headers().get_one("Referer");
|
||||
request::Outcome::Success(Referer(referer.map(|referer| referer.to_owned())))
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{% endif %}
|
||||
{% if user %}
|
||||
<div class="dropdown right">
|
||||
<a href="/users/{{ user.id }}">
|
||||
<a href="/users/{{ user.id }}" class="link">
|
||||
<span>{{ user.username }}</span> <img src="https://cdn.discordapp.com/avatars/{{ user.id }}/{{ user.avatar }}.webp?size=1024">
|
||||
</a>
|
||||
<nav class="dropdown-content">
|
||||
|
@ -30,10 +30,22 @@
|
|||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{% else%}
|
||||
<a href="/login" class="right">Log in</a>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<span class="link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="height: 1.5em">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 21l5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 016-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 01-3.827-5.802" />
|
||||
</svg>
|
||||
</span>
|
||||
<nav class="dropdown-content">
|
||||
{% set langs = 0 | langs %}
|
||||
{% for lang in langs %}
|
||||
<span class="link" onclick="document.cookie = 'lang={{ lang.code }}; expires=Tue, 19 Jan 2038 03:14:07 UTC; SameSite=Strict; path=/'; location.reload()">{{ lang.name }}</span>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</a>
|
||||
</nav>
|
||||
<div>
|
||||
<h1>Welcome to Tegaki Tuesday #{{ challenge }}!</h1>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<dialog>
|
||||
<h1>Welcome to {{ "title" | i18n }}!</h1>
|
||||
<h1>Welcome to {{ "title" | i18n(lang=lang) }}!</h1>
|
||||
<p>In order to participate in challenges, you must be a member of a participating Discord server.</p>
|
||||
<h2>Join a participating server</h2>
|
||||
<div class="servers">
|
||||
|
|
Loading…
Add table
Reference in a new issue