Implement i18n backend

rust
Elnu 1 year ago
parent 1de0633833
commit 76e359ea89

75
Cargo.lock generated

@ -711,6 +711,70 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "encoding"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
dependencies = [
"encoding-index-japanese",
"encoding-index-korean",
"encoding-index-simpchinese",
"encoding-index-singlebyte",
"encoding-index-tradchinese",
]
[[package]]
name = "encoding-index-japanese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-korean"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-simpchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-singlebyte"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-tradchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding_index_tests"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.32" version = "0.8.32"
@ -987,6 +1051,16 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]]
name = "gettext"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ebb594e753d5997e4be036e5a8cf048ab9414352870fb45c779557bbc9ba971"
dependencies = [
"byteorder",
"encoding",
]
[[package]] [[package]]
name = "ghash" name = "ghash"
version = "0.5.0" version = "0.5.0"
@ -3034,6 +3108,7 @@ dependencies = [
"comrak", "comrak",
"derive_more", "derive_more",
"dotenv", "dotenv",
"gettext",
"poise", "poise",
"reqwest", "reqwest",
"rocket 0.5.0-rc.3", "rocket 0.5.0-rc.3",

@ -10,6 +10,7 @@ chrono = { version = "0.4.26", features = ["serde"] }
comrak = "0.18.0" comrak = "0.18.0"
derive_more = "0.99.17" derive_more = "0.99.17"
dotenv = "0.15.0" dotenv = "0.15.0"
gettext = "0.4.0"
poise = "0.5.5" poise = "0.5.5"
reqwest = "0.11.18" reqwest = "0.11.18"
rocket = { version = "=0.5.0-rc.3", features = ["secrets"] } rocket = { version = "=0.5.0-rc.3", features = ["secrets"] }
@ -18,4 +19,4 @@ rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] }
sass-rocket-fairing = "0.2.0" sass-rocket-fairing = "0.2.0"
serde = "1.0.163" serde = "1.0.163"
serde_json = "1.0.96" serde_json = "1.0.96"
serde_yaml = "0.9.21" serde_yaml = "0.9.21"

1
i18n/.gitignore vendored

@ -0,0 +1 @@
*.mo

@ -0,0 +1,2 @@
msgid "title"
msgstr "Tegaki Tuesday"

@ -0,0 +1,2 @@
msgid "title"
msgstr "手書きの火曜日"

@ -17,5 +17,6 @@ pkgs.mkShell {
bacon bacon
pkg-config pkg-config
openssl openssl
gettext
]; ];
} }

@ -0,0 +1,55 @@
use std::{collections::HashMap, fs::{self, File}, io::BufReader, process::Command};
use derive_more::From;
use rocket_dyn_templates::tera::{self, Value};
use gettext::Catalog;
#[derive(From, Debug)]
pub enum LoadCatalogsError {
Io(std::io::Error),
Parse(gettext::Error),
MissingDefaultLanguage,
}
const DEFAULT: &str = "ja";
pub fn load_catalogs() -> Result<HashMap<String, Catalog>, LoadCatalogsError> {
let mut catalogs = HashMap::new();
for file in fs::read_dir("i18n")? {
let file = file?;
let path = file.path();
if !file.file_type()?.is_file() || !path.extension().map(|extension| extension.eq("po")).unwrap_or(false) {
continue;
}
let language_code = path
.file_stem()
.expect("Invalid translation file name")
.to_string_lossy()
.to_string();
let po_file_path = path;
let mo_file_path = format!("i18n/{}.mo", language_code);
Command::new("msgfmt")
.arg(&po_file_path)
.arg("-o")
.arg(&mo_file_path)
.output()?;
catalogs.insert(language_code, Catalog::parse(BufReader::new(File::open(mo_file_path)?))?);
}
if !catalogs.contains_key(DEFAULT) {
return Err(LoadCatalogsError::MissingDefaultLanguage);
}
Ok(catalogs)
}
pub fn i18n_filter(value: &Value, _args: &HashMap<String, Value>, catalogs: &HashMap<String, Catalog>) -> 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()))
}

@ -3,9 +3,9 @@ extern crate rocket;
use poise::serenity_prelude::Http; use poise::serenity_prelude::Http;
use rocket::fs::{relative, FileServer}; use rocket::fs::{relative, FileServer};
use rocket_dyn_templates::Template; use rocket_dyn_templates::{Template, tera};
use sass_rocket_fairing::SassFairing; use sass_rocket_fairing::SassFairing;
use std::env; use std::{env, collections::HashMap};
mod models; mod models;
use models::Settings; use models::Settings;
@ -17,6 +17,9 @@ mod cookies;
mod routes; mod routes;
use routes::*; use routes::*;
mod i18n;
use i18n::{load_catalogs, i18n_filter};
mod prelude; mod prelude;
#[launch] #[launch]
@ -40,6 +43,12 @@ async fn rocket() -> _ {
routes![get_challenge, login, post_login, success, logout, testing], routes![get_challenge, login, post_login, success, logout, testing],
) )
.mount("/css", FileServer::from(relative!("styles/css"))) .mount("/css", FileServer::from(relative!("styles/css")))
.attach(Template::fairing()) .attach(Template::custom(|engines| {
use tera::Value;
let catalogs = load_catalogs().unwrap();
engines.tera.register_filter("i18n", move |value: &Value, args: &HashMap<String, Value>| {
i18n_filter(value, args, &catalogs)
})
}))
.attach(SassFairing::default()) .attach(SassFairing::default())
} }

@ -7,12 +7,6 @@ use crate::models::{Challenge, User, Settings};
#[get("/<challenge>")] #[get("/<challenge>")]
pub async fn get_challenge(challenge: u32, cookies: &CookieJar<'_>, settings: &State<Settings>) -> Template { pub async fn get_challenge(challenge: u32, cookies: &CookieJar<'_>, settings: &State<Settings>) -> Template {
println!(
"{:?}",
cookies
.get_private("user_name")
.map(|cookie| cookie.value().to_owned())
);
Template::render( Template::render(
"index", "index",
context! { context! {

@ -1,5 +1,5 @@
<dialog> <dialog>
<h1>Welcome to Tegaki Tuesday!</h1> <h1>Welcome to {{ "title" | i18n }}!</h1>
<p>In order to participate in challenges, you must be a member of a participating Discord server.</p> <p>In order to participate in challenges, you must be a member of a participating Discord server.</p>
<h2>Join a participating server</h2> <h2>Join a participating server</h2>
<div class="servers"> <div class="servers">

Loading…
Cancel
Save