wip: localization
This commit is contained in:
parent
7cee790c85
commit
1fd5ce890b
12 changed files with 383 additions and 21 deletions
124
Cargo.lock
generated
124
Cargo.lock
generated
|
@ -283,6 +283,17 @@ dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "domain"
|
name = "domain"
|
||||||
version = "0.10.3"
|
version = "0.10.3"
|
||||||
|
@ -334,6 +345,40 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-bundle"
|
||||||
|
version = "0.15.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493"
|
||||||
|
dependencies = [
|
||||||
|
"fluent-langneg",
|
||||||
|
"fluent-syntax",
|
||||||
|
"intl-memoizer",
|
||||||
|
"intl_pluralrules",
|
||||||
|
"rustc-hash",
|
||||||
|
"self_cell 0.10.3",
|
||||||
|
"smallvec",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-langneg"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-syntax"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -573,6 +618,25 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intl-memoizer"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda"
|
||||||
|
dependencies = [
|
||||||
|
"type-map",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intl_pluralrules"
|
||||||
|
version = "7.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
@ -707,12 +771,15 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"bb8",
|
"bb8",
|
||||||
"domain",
|
"domain",
|
||||||
|
"fluent-bundle",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"unic-langid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1004,6 +1071,12 @@ version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -1040,6 +1113,21 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self_cell"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
|
||||||
|
dependencies = [
|
||||||
|
"self_cell 1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self_cell"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
@ -1246,6 +1334,15 @@ version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.42.0"
|
||||||
|
@ -1376,6 +1473,15 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "type-map"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f"
|
||||||
|
dependencies = [
|
||||||
|
"rustc-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
@ -1409,6 +1515,24 @@ version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid-impl"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unic-segment"
|
name = "unic-segment"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
|
@ -20,8 +20,11 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul
|
||||||
#rand = "0.8"
|
#rand = "0.8"
|
||||||
tera = { version = "1", default-features = false }
|
tera = { version = "1", default-features = false }
|
||||||
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
||||||
axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]}
|
axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio", "original-uri" ]}
|
||||||
bb8 = { version = "0.9" }
|
bb8 = { version = "0.9" }
|
||||||
rusqlite = { version = "0.32"}
|
rusqlite = { version = "0.32"}
|
||||||
async-trait = { version = "0.1" }
|
async-trait = { version = "0.1" }
|
||||||
tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
|
tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
|
||||||
|
fluent-bundle = "0.15.3"
|
||||||
|
unic-langid = "*"
|
||||||
|
tower = "*"
|
||||||
|
|
5
locales/en.ftl
Normal file
5
locales/en.ftl
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
zone_records_title = Zone { $zone_name } records
|
||||||
|
|
||||||
|
record_type_address = Addresses
|
||||||
|
record_type_mailserver = E-mail servers
|
||||||
|
record_type_nameserver = Name servers
|
5
locales/fr.ftl
Normal file
5
locales/fr.ftl
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
zone_records_title = Enregistrements de la zone { $zone_name }
|
||||||
|
|
||||||
|
record_type_address = Adresses
|
||||||
|
record_type_mailserver = Serveurs de courriel
|
||||||
|
record_type_nameserver = Serveurs de noms
|
|
@ -372,11 +372,19 @@ impl From<TemplateError> for Error {
|
||||||
fn from(value: TemplateError) -> Self {
|
fn from(value: TemplateError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
TemplateError::RenderError { name, reason } => {
|
TemplateError::RenderError { name, reason } => {
|
||||||
|
let mut this_reason = reason.as_ref();
|
||||||
|
let mut cause = format!("{}", this_reason);
|
||||||
|
|
||||||
|
while let Some(source) = this_reason.source() {
|
||||||
|
cause.push_str(&format!(": {source}"));
|
||||||
|
this_reason = source;
|
||||||
|
}
|
||||||
|
|
||||||
Error::new("template:render", "Failed to render the template")
|
Error::new("template:render", "Failed to render the template")
|
||||||
.with_details(json!({
|
.with_details(json!({
|
||||||
"name": name
|
"name": name
|
||||||
}))
|
}))
|
||||||
.with_cause(&reason.to_string())
|
.with_cause(&cause)
|
||||||
},
|
},
|
||||||
TemplateError::SerializationError { reason } => {
|
TemplateError::SerializationError { reason } => {
|
||||||
Error::new("template:serialization", "Failed to serialize context")
|
Error::new("template:serialization", "Failed to serialize context")
|
||||||
|
|
192
src/locale.rs
Normal file
192
src/locale.rs
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
|
use fluent_bundle::{concurrent::FluentBundle, FluentArgs, FluentResource, FluentValue};
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
const SOURCES: &[(&str, &str)] = &[
|
||||||
|
("en", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/en.ftl"))),
|
||||||
|
("fr", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/locales/fr.ftl"))),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Localization {
|
||||||
|
bundles: std::sync::Arc<HashMap<LanguageIdentifier, FluentBundle<FluentResource>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Localization {
|
||||||
|
pub fn init() -> Self {
|
||||||
|
let mut bundles = HashMap::new();
|
||||||
|
for (locale, translations) in SOURCES {
|
||||||
|
let res = FluentResource::try_new(translations.to_string()).expect("Failed to parse an FTL string.");
|
||||||
|
let langid: LanguageIdentifier = locale.parse().expect("Parsing failed");
|
||||||
|
|
||||||
|
let mut bundle = FluentBundle::new_concurrent(vec![langid.clone()]);
|
||||||
|
|
||||||
|
bundle
|
||||||
|
.add_resource(res)
|
||||||
|
.expect("Failed to add FTL resources to the bundle.");
|
||||||
|
|
||||||
|
bundles.insert(langid, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Localization {
|
||||||
|
bundles: std::sync::Arc::new(bundles),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn language_middleware(&self, default_locale: LanguageIdentifier) -> ExtractLanguageLayer {
|
||||||
|
ExtractLanguageLayer {
|
||||||
|
default_locale,
|
||||||
|
supported_locales: std::sync::Arc::new(self.bundles.keys().cloned().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tera::Function for Localization {
|
||||||
|
fn call(&self, args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
|
||||||
|
let locale = args.get("lang")
|
||||||
|
.and_then(|lang| lang.as_str())
|
||||||
|
.and_then(|lang| LanguageIdentifier::from_str(lang).ok())
|
||||||
|
.ok_or(tera::Error::msg("localize: Missing lang parameter"))?;
|
||||||
|
|
||||||
|
let msg = args.get("msg")
|
||||||
|
.and_then(|lang| lang.as_str())
|
||||||
|
.ok_or(tera::Error::msg("localize: Missing msg parameter"))?;
|
||||||
|
|
||||||
|
let bundle = self.bundles.get(&locale)
|
||||||
|
.ok_or_else(|| tera::Error::msg("localize: Could not find locale {locale}"))?;
|
||||||
|
|
||||||
|
let message = bundle.get_message(msg)
|
||||||
|
.ok_or_else(|| tera::Error::msg("localize: Could not find message {msg}"))?;
|
||||||
|
|
||||||
|
|
||||||
|
let pattern = message.value()
|
||||||
|
.ok_or_else(|| tera::Error::msg("localize: Message {msg} has no value"))?;
|
||||||
|
|
||||||
|
let mut msg_args = FluentArgs::new();
|
||||||
|
|
||||||
|
for (key, value) in args {
|
||||||
|
if key != "msg" && key != "lang" {
|
||||||
|
msg_args.set(key, fluent_value_from_tera(value, key)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let localized_message = bundle.format_pattern(pattern, Some(&msg_args), &mut errors);
|
||||||
|
|
||||||
|
for err in errors {
|
||||||
|
eprint!("[warn] localization error: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tera::Value::from(localized_message))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fluent_value_from_tera<'s>(value: &'s tera::Value, name: &str) -> tera::Result<FluentValue<'s>> {
|
||||||
|
match value {
|
||||||
|
tera::Value::String(string) => Ok(FluentValue::from(string)),
|
||||||
|
tera::Value::Number(number) => Ok(FluentValue::from(number.as_f64())),
|
||||||
|
_ => Err(tera::Error::msg(format!("localize: Argument {name} can only be a string or a number"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ExtractLanguageLayer {
|
||||||
|
default_locale: LanguageIdentifier,
|
||||||
|
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> tower::Layer<S> for ExtractLanguageLayer {
|
||||||
|
|
||||||
|
type Service = ExtractLanguageService<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
ExtractLanguageService {
|
||||||
|
inner,
|
||||||
|
default_locale: self.default_locale.clone(),
|
||||||
|
supported_locales: self.supported_locales.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ExtractLanguageService<S> {
|
||||||
|
inner: S,
|
||||||
|
default_locale: LanguageIdentifier,
|
||||||
|
supported_locales: std::sync::Arc<Vec<LanguageIdentifier>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ExtractLanguageService<S> {
|
||||||
|
// https://httpwg.org/specs/rfc9110.html#field.accept-language
|
||||||
|
pub fn language_from_header(&self, headers: &axum::http::HeaderMap) -> LanguageIdentifier {
|
||||||
|
let lang_preferences = headers
|
||||||
|
.get("Accept-Language")
|
||||||
|
.and_then(|val| val.to_str().ok());
|
||||||
|
|
||||||
|
if let Some(lang_preferences) = lang_preferences {
|
||||||
|
let mut languages = Vec::new();
|
||||||
|
|
||||||
|
for lang_item in lang_preferences.split(",") {
|
||||||
|
let lang_item = lang_item.trim();
|
||||||
|
let lang_config = lang_item.split_once(';');
|
||||||
|
if let Some((lang_id, config)) = lang_config {
|
||||||
|
let lang_id = lang_id.trim();
|
||||||
|
let preference = config.trim().strip_prefix("q=")
|
||||||
|
.and_then(|value| value.parse::<f32>().ok());
|
||||||
|
if let Some(preference) = preference {
|
||||||
|
languages.push((lang_id.trim(), preference));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
languages.push((lang_item, 1f32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
languages.sort_by(|l1, l2| l1.1.total_cmp(&l2.1));
|
||||||
|
|
||||||
|
let iter = languages.into_iter()
|
||||||
|
.rev()
|
||||||
|
.filter(|l| l.1 >= 0.001 && l.1 <= 1f32)
|
||||||
|
.filter_map(|(lang, _)| LanguageIdentifier::from_str(lang).ok());
|
||||||
|
|
||||||
|
for lang in iter {
|
||||||
|
for supported_lang in self.supported_locales.iter() {
|
||||||
|
if lang == *supported_lang {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.default_locale.clone()
|
||||||
|
} else {
|
||||||
|
self.default_locale.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> tower::Service<axum::extract::Request> for ExtractLanguageService<S>
|
||||||
|
where
|
||||||
|
S: tower::Service<axum::extract::Request, Response = axum::response::Response> + Send + 'static,
|
||||||
|
S::Future: Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
// `BoxFuture` is a type alias for `Pin<Box<dyn Future + Send + 'a>>`
|
||||||
|
type Future = std::pin::Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, mut req: axum::extract::Request) -> Self::Future {
|
||||||
|
let headers = req.headers();
|
||||||
|
|
||||||
|
let lang = self.language_from_header(headers);
|
||||||
|
|
||||||
|
req.extensions_mut().insert(lang);
|
||||||
|
|
||||||
|
Box::pin(self.inner.call(req))
|
||||||
|
}
|
||||||
|
}
|
11
src/main.rs
11
src/main.rs
|
@ -7,6 +7,7 @@ mod validation;
|
||||||
mod macros;
|
mod macros;
|
||||||
mod template;
|
mod template;
|
||||||
mod proto;
|
mod proto;
|
||||||
|
mod locale;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -32,7 +33,12 @@ pub struct AppState {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let template_engine = TemplateEngine::new(std::path::Path::new("./templates"));
|
let localization = locale::Localization::init();
|
||||||
|
|
||||||
|
let template_engine = TemplateEngine::new(
|
||||||
|
std::path::Path::new("./templates"),
|
||||||
|
localization.clone()
|
||||||
|
);
|
||||||
|
|
||||||
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
|
let dns_driver = dns::dns_driver::DnsDriver::from_config(DnsDriverConfig {
|
||||||
address: "127.0.0.1:5353".parse().unwrap(),
|
address: "127.0.0.1:5353".parse().unwrap(),
|
||||||
|
@ -61,7 +67,8 @@ async fn main() {
|
||||||
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page))
|
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page))
|
||||||
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page))
|
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page))
|
||||||
.nest_service("/assets", ServeDir::new("assets"))
|
.nest_service("/assets", ServeDir::new("assets"))
|
||||||
.with_state(app_state);
|
.with_state(app_state)
|
||||||
|
.layer(localization.language_middleware("en".parse().unwrap()));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use axum::extract::{Query, Path, State};
|
use axum::extract::{Query, Path, State, OriginalUri};
|
||||||
|
use axum::Extension;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
use crate::validation;
|
use crate::validation;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
@ -12,19 +14,23 @@ use crate::resources::dns::friendly;
|
||||||
pub async fn get_records_page(
|
pub async fn get_records_page(
|
||||||
Path(zone_name): Path<String>,
|
Path(zone_name): Path<String>,
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
request: Request,
|
OriginalUri(url): OriginalUri,
|
||||||
|
Extension(lang): Extension<LanguageIdentifier>,
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
) -> Result<Template<'static, Value>, Error> {
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||||
let records = zone.get_records(app.records).await?;
|
let records = zone.get_records(app.records).await?;
|
||||||
let records = friendly::FriendlyRecords::from(records.clone());
|
let records = friendly::FriendlyRecords::from(records.clone());
|
||||||
|
|
||||||
|
println!("{}", lang);
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/records.html",
|
"pages/records.html",
|
||||||
app.template_engine,
|
app.template_engine,
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone.name,
|
"current_zone": zone.name,
|
||||||
"records": records,
|
"records": records,
|
||||||
"url": request.uri().to_string(),
|
"url": url.to_string(),
|
||||||
|
"lang": lang.to_string(),
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -40,7 +46,8 @@ pub async fn get_new_record_page(
|
||||||
Path(zone_name): Path<String>,
|
Path(zone_name): Path<String>,
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
Query(params): Query<NewRecordQuery>,
|
Query(params): Query<NewRecordQuery>,
|
||||||
request: Request,
|
OriginalUri(url): OriginalUri,
|
||||||
|
Extension(lang): Extension<LanguageIdentifier>,
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
) -> Result<Template<'static, Value>, Error> {
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||||
|
|
||||||
|
@ -60,6 +67,8 @@ pub async fn get_new_record_page(
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
println!("{}", lang);
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/new_record.html",
|
"pages/new_record.html",
|
||||||
app.template_engine,
|
app.template_engine,
|
||||||
|
@ -69,7 +78,8 @@ pub async fn get_new_record_page(
|
||||||
"domain_error": domain_error,
|
"domain_error": domain_error,
|
||||||
"config": params.config,
|
"config": params.config,
|
||||||
"rtype": params.rtype,
|
"rtype": params.rtype,
|
||||||
"url": request.uri().to_string(),
|
"url": url.to_string(),
|
||||||
|
"lang": lang.to_string(),
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,28 @@ use axum::response::{Html, IntoResponse};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tera::{Tera, Context};
|
use tera::{Tera, Context};
|
||||||
|
|
||||||
use crate::errors::Error;
|
use crate::{errors::Error, locale::Localization};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TemplateEngine {
|
pub struct TemplateEngine {
|
||||||
tera: Arc<Tera>,
|
pub tera: Arc<Tera>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum TemplateError {
|
pub enum TemplateError {
|
||||||
SerializationError { reason: Box<dyn std::error::Error> },
|
SerializationError { reason: Box<dyn std::error::Error> },
|
||||||
RenderError { name: String, reason: Box<dyn std::error::Error> },
|
RenderError { name: String, reason: Box<dyn std::error::Error> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateEngine {
|
impl TemplateEngine {
|
||||||
pub fn new(template_directory: &Path) -> Self {
|
pub fn new(template_directory: &Path, localization: Localization) -> Self {
|
||||||
let template_glob = template_directory.join("**").join("*");
|
let template_glob = template_directory.join("**").join("*");
|
||||||
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
match Tera::new(template_glob.to_str().expect("valid glob path string")) {
|
||||||
Ok(tera) => TemplateEngine { tera: Arc::new(tera) },
|
Ok(mut tera) => {
|
||||||
|
tera.register_function("tr", localization);
|
||||||
|
TemplateEngine { tera: Arc::new(tera) }
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Loading templates failed: {}", e);
|
println!("Loading templates failed: {}", e);
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="{{ lang }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% macro rrset(rtype, ttl, data, zone) %}
|
{% macro rrset(rtype, ttl, data, zone, lang) %}
|
||||||
<li class="rrset">
|
<li class="rrset">
|
||||||
<div class="rtype">
|
<div class="rtype">
|
||||||
{{ rtype }}
|
{{ tr(msg="record_type_" ~ rtype, lang=lang) }}
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<button class="icon">
|
<button class="icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% block title %}Records - {{ current_zone }} - {% endblock title %}
|
{% block title %}Records - {{ current_zone }} - {% endblock title %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<h1>Zone <strong>{{ current_zone }}</strong> records</h1>
|
<h1>{{ tr(msg="zone_records_title", lang=lang, zone_name="<strong>" ~ current_zoned ~ "</strong>") | safe }}</h1>
|
||||||
<svg width="0" height="0" aria-hidden="true" style="position: absolute;">
|
<svg width="0" height="0" aria-hidden="true" style="position: absolute;">
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox">
|
<clipPath id="corner-folder-tab-right" clipPathUnits="objectBoundingBox">
|
||||||
|
@ -36,7 +36,8 @@
|
||||||
rtype=group.web.addresses.rtype,
|
rtype=group.web.addresses.rtype,
|
||||||
ttl=group.web.addresses.ttl,
|
ttl=group.web.addresses.ttl,
|
||||||
data=group.web.addresses.data,
|
data=group.web.addresses.data,
|
||||||
zone=current_zone) }}
|
zone=current_zone,
|
||||||
|
lang=lang) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -49,14 +50,16 @@
|
||||||
rtype=group.mail.servers.rtype,
|
rtype=group.mail.servers.rtype,
|
||||||
ttl=group.mail.servers.ttl,
|
ttl=group.mail.servers.ttl,
|
||||||
data=group.mail.servers.data,
|
data=group.mail.servers.data,
|
||||||
zone=current_zone) }}
|
zone=current_zone,
|
||||||
|
lang=lang) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if group.mail.spf %}
|
{% if group.mail.spf %}
|
||||||
{{ macros::rrset(
|
{{ macros::rrset(
|
||||||
rtype=group.mail.spf.rtype,
|
rtype=group.mail.spf.rtype,
|
||||||
ttl=group.mail.spf.ttl,
|
ttl=group.mail.spf.ttl,
|
||||||
data=group.mail.spf.data,
|
data=group.mail.spf.data,
|
||||||
zone=current_zone) }}
|
zone=current_zone,
|
||||||
|
lang=lang) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -69,7 +72,8 @@
|
||||||
rtype=rrset.rtype,
|
rtype=rrset.rtype,
|
||||||
ttl=rrset.ttl,
|
ttl=rrset.ttl,
|
||||||
data=rrset.data,
|
data=rrset.data,
|
||||||
zone=current_zone) }}
|
zone=current_zone,
|
||||||
|
lang=lang) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in a new issue