Working localization

rust
Elnu 1 year ago
parent 245fcbcf1e
commit 3abc45cd4f

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";

@ -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);
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,
}
Ok(Value::String(translation.to_owned()))
pub fn langs_filter(
_value: &Value,
_args: &HashMap<String, Value>,
langs: &Vec<LangCode>,
) -> tera::Result<Value> {
Ok(serde_json::to_value(langs).unwrap())
}

@ -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),
},

@ -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…
Cancel
Save