Merge pull request #6 from enaut/WIP--translations

Make the page translatable
This commit is contained in:
Franz Dietrich 2021-03-31 16:50:13 +02:00 committed by GitHub
commit 053992e30c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 579 additions and 57 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
/target /target
.env .env
links.db links.db*
launch.json launch.json
settings.json settings.json
links.session.sql links.session.sql

295
Cargo.lock generated
View File

@ -1,5 +1,15 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "actix-codec" name = "actix-codec"
version = "0.3.0" version = "0.3.0"
@ -412,6 +422,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "arc-swap"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d7d63395147b81a9e570bcc6243aaf71c017bd666d4909cfef0085bdda8d73"
[[package]] [[package]]
name = "argonautica" name = "argonautica"
version = "0.2.0" version = "0.2.0"
@ -1037,6 +1053,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "dotenv" name = "dotenv"
version = "0.15.0" version = "0.15.0"
@ -1126,6 +1148,99 @@ dependencies = [
"miniz_oxide 0.4.3", "miniz_oxide 0.4.3",
] ]
[[package]]
name = "fluent"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960ac6317b829b94c67f9a774e8b56db388405e174855a5a84d4b461ff85b281"
dependencies = [
"fluent-bundle",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3cc2d1c59a0daaa93bb346db97e1ebad1067c5ffedc1af8b937a9d8caa6a77"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"ouroboros",
"rustc-hash",
"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.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784f660373ea898f712a7e67b43f35bf79608d46112747c29767d087611d716b"
[[package]]
name = "fluent-template-macros"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3051f88dd918b30a994177acc8f3b8b0b399c98ca5aa2ab985e79070a8841592"
dependencies = [
"flume",
"ignore",
"once_cell",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn",
"unic-langid",
]
[[package]]
name = "fluent-templates"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fa6b2186b959236019d67fb839f036c83a799edd4389c505678ca1b8d41e9ed"
dependencies = [
"arc-swap",
"fluent",
"fluent-bundle",
"fluent-langneg",
"fluent-syntax",
"fluent-template-macros",
"flume",
"heck",
"ignore",
"lazy_static",
"log",
"once_cell",
"serde_json",
"snafu",
"tera",
"unic-langid",
]
[[package]]
name = "flume"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a685ab99b8f60a271b44d5dd1a76e55124a8c9fa0407b7a8e9cd172d5b588"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project 1.0.4",
"spinning_top",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -1327,8 +1442,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"js-sys",
"libc", "libc",
"wasi 0.10.2+wasi-snapshot-preview1", "wasi 0.10.2+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1589,6 +1706,26 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "intl-memoizer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18f988384267d7066cc2be425e6faf352900652c046b6971d2e228d3b1c5ecf"
dependencies = [
"tinystr",
"unic-langid",
]
[[package]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.4" version = "0.1.4"
@ -1848,6 +1985,15 @@ dependencies = [
"ws2_32-sys", "ws2_32-sys",
] ]
[[package]]
name = "nanorand"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1378b66f7c93a1c0f8464a19bf47df8795083842e5090f4b7305973d5a22d0"
dependencies = [
"getrandom 0.2.2",
]
[[package]] [[package]]
name = "net2" name = "net2"
version = "0.2.37" version = "0.2.37"
@ -1957,6 +2103,29 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "ouroboros"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d5c203fe8d786d9d7bec8203cbbff3eb2cf8410c0d70cfd05b3d5f5d545da"
dependencies = [
"ouroboros_macro",
"stable_deref_trait",
]
[[package]]
name = "ouroboros_macro"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "129943a960e6a08c7e70ca5a09f113c273fe7f10ae8420992c78293e3dffdf65"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.1" version = "0.11.1"
@ -2145,6 +2314,30 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn",
"version_check 0.9.2",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"version_check 0.9.2",
]
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"
@ -2187,6 +2380,8 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"dotenv", "dotenv",
"fluent-langneg",
"fluent-templates",
"image", "image",
"qrcode", "qrcode",
"rand 0.8.3", "rand 0.8.3",
@ -2566,6 +2761,12 @@ version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[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.2.3" version = "0.2.3"
@ -2820,6 +3021,27 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "snafu"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7"
dependencies = [
"doc-comment",
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.8",
"syn",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.3.19" version = "0.3.19"
@ -2837,6 +3059,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spinning_top"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "sqlformat" name = "sqlformat"
version = "0.1.6" version = "0.1.6"
@ -2941,6 +3172,12 @@ dependencies = [
"tokio-rustls", "tokio-rustls",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "standback" name = "standback"
version = "0.2.14" version = "0.2.14"
@ -3220,6 +3457,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tinystr"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1"
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.1.1" version = "1.1.1"
@ -3365,6 +3608,15 @@ dependencies = [
"trust-dns-proto", "trust-dns-proto",
] ]
[[package]]
name = "type-map"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46"
dependencies = [
"rustc-hash",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.12.0" version = "1.12.0"
@ -3398,6 +3650,49 @@ 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.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5"
dependencies = [
"unic-langid-impl",
"unic-langid-macros",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d"
dependencies = [
"tinystr",
]
[[package]]
name = "unic-langid-macros"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f980d6d87e8805f2836d64b4138cc95aa7986fa63b1f51f67d5fbff64dd6e5"
dependencies = [
"proc-macro-hack",
"tinystr",
"unic-langid-impl",
"unic-langid-macros-impl",
]
[[package]]
name = "unic-langid-macros-impl"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29396ffd97e27574c3e01368b1a64267d3064969e4848e2e130ff668be9daa9f"
dependencies = [
"proc-macro-hack",
"quote 1.0.8",
"syn",
"unic-langid-impl",
]
[[package]] [[package]]
name = "unic-segment" name = "unic-segment"
version = "0.9.0" version = "0.9.0"

View File

@ -33,6 +33,8 @@ image = "0.23"
rand="0.8" rand="0.8"
rpassword = "5.0" rpassword = "5.0"
clap = "2.33" clap = "2.33"
fluent-templates = { version = "0.6", features = ["tera"] }
fluent-langneg ="0.13"
[build-dependencies] [build-dependencies]
actix-web-static-files = "3.0" actix-web-static-files = "3.0"

36
locales/de/main.ftl Normal file
View File

@ -0,0 +1,36 @@
list-links = Link Liste
add-link = Link hinzufügen
invite-user = Benutzer einladen
list-users = Liste der Benutzer
welcome-user = Herzlich willkommen {$username}
logout = Abmelden
login = Login
not-found = Dieser Link existiert nicht, oder wurde gelöscht.
edit-link-headline = Zu editierender Link: {$linktitle}
edit-link = Link Editieren
link-description = Beschreibung
link-target = Link Ziel
link-code = Link Code
shortlink = Shortlink
qr-code = QR-code
danger-zone = Achtung!
danger-zone-text = Verändern Sie den Code von bereits veröffentlichten Links nicht. Sollte es dennoch geschehen werden veröffentlichte links unbenutzbar. Wird das Linkziel verändert, so zeigen auch die bereits veröffentlichten Links auf das neue Ziel.
save-edits = Speichere die Veränderungen
delete-link = Diesen Link löschen
edit-user-headline = Benutzereinstellungen von: {$username}
username = Benutzername
email = Email
password = Passwort
password-placeholder = Leer lassen um das Passwort nicht zu ändern
save-user = Benutzer speichern
edit-user = Benutzer editieren
make-user-admin = Zum Administrator befördern
make-user-regular = Zurückstufen zum normalen Nutzer
userid = Benutzernummer
statistics = Statistik

36
locales/en/main.ftl Normal file
View File

@ -0,0 +1,36 @@
list-links = List of existing links
add-link = Add a new link
invite-user = Invite a new user
list-users = List of existing users
welcome-user = Welcome {$username}
logout = Logout
login = Login
not-found = This Link has not been found or has been deleted
edit-link-headline = Edit link: {$linktitle}
edit-link = Edit link
link-description = Description
link-target = Link target
link-code = Link code
shortlink = Shortlink
danger-zone = Danger Zone!
danger-zone-text = Do not change the code of links that are published. If you do so the published links will become invalid! If you change the target the published links will point to the new target.
save-edits = Save edits
delete-link = Delete this link
user-headline = User Settings of: {$username}
edit-user-headline = Change Settings of: {$username}
username = Username
email = Email
password = Password
password-placeholder = Leave this empty to keep the current password
save-user = Save this user
edit-user = Edit this user
make-user-admin = Promote to admin
make-user-regular = Demote to regular
userid = User ID
statistics = Statistics

View File

@ -0,0 +1,5 @@
-- Add migration script here
ALTER TABLE
users
ADD
COLUMN language Text NOT NULL DEFAULT "en";

View File

@ -25,6 +25,7 @@ use std::{fmt::Display, path::PathBuf, str::FromStr};
use actix_identity::{CookieIdentityPolicy, IdentityService}; use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{web, App, HttpResponse, HttpServer}; use actix_web::{web, App, HttpResponse, HttpServer};
use fluent_templates::{static_loader, FluentLoader};
use qrcode::types::QrError; use qrcode::types::QrError;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tera::Tera; use tera::Tera;
@ -188,9 +189,20 @@ impl ServerConfig {
} }
include!(concat!(env!("OUT_DIR"), "/generated.rs")); include!(concat!(env!("OUT_DIR"), "/generated.rs"));
static_loader! {
static LOCALES = {
locales: "./locales",
fallback_language: "en",
};
}
fn build_tera() -> Tera { fn build_tera() -> Tera {
let mut tera = Tera::default(); let mut tera = Tera::default();
// Add translation support
tera.register_function("fluent", FluentLoader::new(&*LOCALES));
tera.add_raw_templates(vec![ tera.add_raw_templates(vec![
("admin.html", include_str!("../templates/admin.html")), ("admin.html", include_str!("../templates/admin.html")),
("base.html", include_str!("../templates/base.html")), ("base.html", include_str!("../templates/base.html")),
@ -311,7 +323,11 @@ async fn webservice(server_config: ServerConfig) -> std::io::Result<()> {
web::post().to(views::process_edit_profile), web::post().to(views::process_edit_profile),
), ),
) )
.route("/set_admin/{user_id}", web::get().to(views::toggle_admin)), .route("/set_admin/{user_id}", web::get().to(views::toggle_admin))
.route(
"/set_language/{language}",
web::get().to(views::set_language),
),
) )
.service( .service(
web::scope("/delete").service( web::scope("/delete").service(

View File

@ -11,6 +11,7 @@ pub struct User {
pub email: String, pub email: String,
pub password: String, pub password: String,
pub role: i64, pub role: i64,
pub language: String,
} }
impl User { impl User {
@ -62,6 +63,7 @@ impl User {
.await?; .await?;
Ok(()) Ok(())
} }
pub(crate) async fn toggle_admin( pub(crate) async fn toggle_admin(
self, self,
server_config: &ServerConfig, server_config: &ServerConfig,
@ -73,6 +75,21 @@ impl User {
Ok(()) Ok(())
} }
pub(crate) async fn set_language(
self,
server_config: &ServerConfig,
new_language: &str,
) -> Result<(), ServerError> {
sqlx::query!(
"UPDATE users SET language = ? where id = ?",
new_language,
self.id
)
.execute(&server_config.db_pool)
.await?;
Ok(())
}
pub(crate) async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> { pub(crate) async fn count_admins(server_config: &ServerConfig) -> Result<Count, ServerError> {
let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2") let num = sqlx::query_as!(Count, "select count(*) as number from users where role = 2")
.fetch_one(&server_config.db_pool) .fetch_one(&server_config.db_pool)

View File

@ -81,6 +81,7 @@ pub(crate) async fn list_all_allowed(
users.username as usern, users.username as usern,
users.email as uemail, users.email as uemail,
users.role as urole, users.role as urole,
users.language as ulang,
count(clicks.id) as counter count(clicks.id) as counter
from from
links links
@ -107,6 +108,7 @@ pub(crate) async fn list_all_allowed(
email: v.get("uemail"), email: v.get("uemail"),
password: "invalid".to_owned(), password: "invalid".to_owned(),
role: v.get("urole"), role: v.get("urole"),
language: v.get("ulang"),
}, },
clicks: Count { clicks: Count {
number: v.get("counter"), /* count is never None */ number: v.get("counter"), /* count is never None */
@ -244,6 +246,7 @@ pub(crate) async fn update_user(
email: data.email.clone(), email: data.email.clone(),
password, password,
role: unmodified_user.role, role: unmodified_user.role,
language: unmodified_user.language,
}; };
new_user.update_user(server_config).await?; new_user.update_user(server_config).await?;
let changed_user = User::get_user(uid, server_config).await?; let changed_user = User::get_user(uid, server_config).await?;
@ -302,6 +305,26 @@ pub(crate) async fn toggle_admin(
} }
} }
pub(crate) async fn set_language(
id: &Identity,
lang_code: &str,
server_config: &ServerConfig,
) -> Result<(), ServerError> {
match lang_code {
"de" | "en" => match authenticate(id, server_config).await? {
Role::Admin { user } | Role::Regular { user } => {
user.set_language(server_config, lang_code).await
}
Role::Disabled | Role::NotAuthenticated => {
Err(ServerError::User("Not Allowed".to_owned()))
}
},
_ => Err(ServerError::User(
"This language is not supported!".to_owned(),
)),
}
}
/// Get one link if permissions are accordingly. /// Get one link if permissions are accordingly.
pub(crate) async fn get_link( pub(crate) async fn get_link(
id: &Identity, id: &Identity,

View File

@ -3,9 +3,14 @@ use std::time::SystemTime;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{ use actix_web::{
http::header::{CacheControl, CacheDirective, ContentType, Expires}, http::header::{CacheControl, CacheDirective, ContentType, Expires},
web, HttpResponse, web, HttpRequest, HttpResponse,
}; };
use argonautica::Verifier; use argonautica::Verifier;
use fluent_langneg::{
convert_vec_str_to_langids_lossy, negotiate_languages, parse_accepted_languages,
NegotiationStrategy,
};
use fluent_templates::LanguageIdentifier;
use image::{DynamicImage, ImageOutputFormat, Luma}; use image::{DynamicImage, ImageOutputFormat, Luma};
use qrcode::{render::svg, QrCode}; use qrcode::{render::svg, QrCode};
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -27,6 +32,35 @@ fn redirect_builder(target: &str) -> HttpResponse {
.body(format!("Redirect to {}", target)) .body(format!("Redirect to {}", target))
} }
fn detect_language(request: &HttpRequest) -> Result<String, ServerError> {
let requested = parse_accepted_languages(
request
.headers()
.get(actix_web::http::header::ACCEPT_LANGUAGE)
.ok_or_else(|| ServerError::User("Failed to get Accept_Language".to_owned()))?
.to_str()
.map_err(|_| {
ServerError::User("Failed to convert Accept_language to str".to_owned())
})?,
);
let available = convert_vec_str_to_langids_lossy(&["de", "en"]);
let default: LanguageIdentifier = "en"
.parse()
.map_err(|_| ServerError::User("Failed to parse a langid.".to_owned()))?;
let supported = negotiate_languages(
&requested,
&available,
Some(&default),
NegotiationStrategy::Filtering,
);
let languagecode = supported
.get(0)
.map_or("en".to_string(), std::string::ToString::to_string);
println!("Detected the language: {}", &languagecode);
Ok(languagecode)
}
/// Show the list of all available links if a user is authenticated /// Show the list of all available links if a user is authenticated
pub(crate) async fn index( pub(crate) async fn index(
tera: web::Data<Tera>, tera: web::Data<Tera>,
@ -248,12 +282,26 @@ pub(crate) async fn toggle_admin(
))) )))
} }
pub(crate) async fn set_language(
data: web::Path<String>,
config: web::Data<crate::ServerConfig>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
queries::set_language(&id, &data.0, &config).await?;
Ok(redirect_builder("/admin/index/"))
}
pub(crate) async fn login( pub(crate) async fn login(
tera: web::Data<Tera>, tera: web::Data<Tera>,
id: Identity, id: Identity,
config: web::Data<crate::ServerConfig>,
req: HttpRequest,
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
let language_code = detect_language(&req)?;
slog_info!(config.log, "Detected languagecode: {}", &language_code);
let mut data = Context::new(); let mut data = Context::new();
data.insert("title", "Login"); data.insert("title", "Login");
data.insert("language", &language_code);
if let Some(_id) = id.identity() { if let Some(_id) = id.identity() {
return Ok(redirect_builder("/admin/index/")); return Ok(redirect_builder("/admin/index/"));
@ -304,6 +352,7 @@ pub(crate) async fn redirect(
tera: web::Data<Tera>, tera: web::Data<Tera>,
config: web::Data<crate::ServerConfig>, config: web::Data<crate::ServerConfig>,
data: web::Path<String>, data: web::Path<String>,
req: HttpRequest,
) -> Result<HttpResponse, ServerError> { ) -> Result<HttpResponse, ServerError> {
slog_info!(config.log, "Redirecting to {:?}", data); slog_info!(config.log, "Redirecting to {:?}", data);
let link = queries::get_link_simple(&data.0, &config).await; let link = queries::get_link_simple(&data.0, &config).await;
@ -323,6 +372,8 @@ pub(crate) async fn redirect(
); );
let mut data = Context::new(); let mut data = Context::new();
data.insert("title", "Wurde gel\u{f6}scht"); data.insert("title", "Wurde gel\u{f6}scht");
let language = detect_language(&req)?;
data.insert("language", &language);
let rendered = tera.render("not_found.html", &data)?; let rendered = tera.render("not_found.html", &data)?;
Ok(HttpResponse::NotFound().body(rendered)) Ok(HttpResponse::NotFound().body(rendered))
} }

View File

@ -11,14 +11,19 @@
<div class="admin"> <div class="admin">
<nav> <nav>
<ol> <ol>
<li><a href="/admin/index/">Link Liste</a></li> <li><a href="/admin/index/">{{ fluent(key="list-links", lang=user.language) }}</a>
<li><a href="/admin/submit/">Link Hinzufügen</a></li>
{% if user.role == 2 %}<li><a href="/admin/signup/">Einladen</a></li>
<li><a href="/admin/view/users/">Benutzer</a></li>{% endif %}
<li style="float:right"><a href="/admin/logout/">Abmelden</a></li>
<li style="float:right">
<div class="willkommen">Herzlich willkommen {{ user.username }}</div>
</li> </li>
<li><a href="/admin/submit/">{{ fluent(key="add-link", lang=user.language) }}</a></li>
{% if user.role == 2 %}<li><a href="/admin/signup/">{{ fluent(key="invite-user", lang=user.language) }}</a>
</li>
<li><a href="/admin/view/users/">{{ fluent(key="list-users", lang=user.language) }}</a></li>{% endif %}
<li style="float:right"><a href="/admin/logout/">{{ fluent(key="logout", lang=user.language) }}</a></li>
<li style="float:right">
<div class="willkommen">{{ fluent(key="welcome-user", lang=user.language, username=user.username) }}
</div>
</li>
<li style="float:right"><a href="/admin/edit/set_language/en">en</a></li>
<li style="float:right"><a href="/admin/edit/set_language/de">de</a></li>
</ol> </ol>
</nav> </nav>
{% block admin %} {% block admin %}

View File

@ -2,26 +2,26 @@
{% block admin %} {% block admin %}
<div class="center"> <div class="center">
<h1>Link Editieren: {{ link.title }}</h1> <h1>{{ fluent(key="edit-link-headline", lang=user.language, linktitle=link.title) }}</h1>
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="title">Beschreibung:</label> <label for="title">{{ fluent(key="link-description", lang=user.language) }}:</label>
<input type="text" name="title" value="{{ link.title }}"> <input type="text" name="title" value="{{ link.title }}">
</div> </div>
<div> <div>
<label for="target">Ziel:</label> <label for="target">{{ fluent(key="link-target", lang=user.language) }}:</label>
<input type="text" name="target" value="{{link.target}}"> <input type="text" name="target" value="{{link.target}}">
</div> </div>
<div> <div>
<label for="code">Code:</label> <label for="code">{{ fluent(key="link-code", lang=user.language) }}:</label>
<input type="text" name="code" value="{{link.code}}"> <input type="text" name="code" value="{{link.code}}">
</div> </div>
<div class="actions danger"> <div class="actions danger">
<h2>Achtung!</h2> <h2>{{ fluent(key="danger-zone", lang=user.language) }}</h2>
<h3>Werden schon veröffentlichte Links gelöscht oder editiert sind die Links z.B. aus einem Buch <h3>{{ fluent(key="danger-zone-text", lang=user.language) }}</h3>
nicht mehr gültig! UNBEDINGT VERMEIDEN!</h3> <input type="submit" value='{{ fluent(key="save-edits", lang=user.language) }}'>
<input type="submit" value="Speichern"> <a class="button" href="/admin/delete/link/{{link.code}}">{{ fluent(key="delete-link", lang=user.language)
<a class="button" href="/admin/delete/link/{{link.code}}">Delete</a> }}</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -2,21 +2,27 @@
{% block admin %} {% block admin %}
<div class="center"> <div class="center">
<h1>Profil von {{user.username}}</h1> <h1>{{ fluent(key="edit-user-headline", lang=user.language, username=user.username) }}
</h1>
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="username">Benutzername:</label> <label for="username">{{ fluent(key="username", lang=user.language)
}}:</label>
<input type="text" name="username" value="{{ user.username }}"> <input type="text" name="username" value="{{ user.username }}">
</div> </div>
<div> <div>
<label for="email">E-mail:</label> <label for="email">{{ fluent(key="email", lang=user.language)
}}:</label>
<input type="email" name="email" value="{{ user.email }}"> <input type="email" name="email" value="{{ user.email }}">
</div> </div>
<div> <div>
<label for="password">Passwort:</label> <label for="password">{{ fluent(key="password", lang=user.language)
<input type="password" name="password" placeholder="Leer lassen um nichts zu ändern"> }}:</label>
<input type="password" name="password" placeholder='{{ fluent(key="password-placeholder", lang=user.language)
}}'>
</div> </div>
<input type="submit" value="Speichern"> <input type="submit" value='{{ fluent(key="save-user", lang=user.language)
}}'>
</form> </form>
<h2>&nbsp;</h2> <h2>&nbsp;</h2>
</div> </div>

View File

@ -11,16 +11,20 @@
<tr> <tr>
<th> <th>
Kürzel {{ fluent(key="link-code", lang=user.language)
}}
</th> </th>
<th> <th>
Ziellink {{ fluent(key="link-target", lang=user.language)
}}
</th> </th>
<th> <th>
Benutzername {{ fluent(key="username", lang=user.language)
}}
</th> </th>
<th> <th>
Statistik {{ fluent(key="statistics", lang=user.language)
}}
</th> </th>
</tr> </tr>
{% for links_user in links_per_users %} {% for links_user in links_per_users %}

View File

@ -11,13 +11,16 @@
<tr> <tr>
<th> <th>
Kürzel {{ fluent(key="userid", lang=user.language)
}}
</th> </th>
<th> <th>
Emailadresse {{ fluent(key="email", lang=user.language)
}}
</th> </th>
<th> <th>
Benutzername {{ fluent(key="username", lang=user.language)
}}
</th> </th>
</tr> </tr>
{% for user in users %} {% for user in users %}

View File

@ -4,14 +4,16 @@
<div class="center"> <div class="center">
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="username">Benutzername:</label> <label for="username">{{ fluent(key="username", lang=language)
}}:</label>
<input type="text" name="username"> <input type="text" name="username">
</div> </div>
<div> <div>
<label for="password">Passwort:</label> <label for="password">{{ fluent(key="password", lang=language)
}}:</label>
<input type="password" name="password"> <input type="password" name="password">
</div> </div>
<input type="submit" value="Login"> <input type="submit" value='{{ fluent(key="login", lang=language) }}'>
</form> </form>
<h2>&nbsp;</h2> <h2>&nbsp;</h2>
</div> </div>

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="center"> <div class="center">
<h3>This Link has not been found or has been deleted</h3> <h3>{{ fluent(key="not-found", lang=language) }}</h3>
<h2>&nbsp;</h2> <h2>&nbsp;</h2>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,18 +4,22 @@
<div class="center"> <div class="center">
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="username">Benutzername:</label> <label for="username">{{ fluent(key="username", lang=user.language)
}}:</label>
<input type="text" name="username"> <input type="text" name="username">
</div> </div>
<div> <div>
<label for="email">E-mail:</label> <label for="email">{{ fluent(key="email", lang=user.language)
}}:</label>
<input type="email" name="email"> <input type="email" name="email">
</div> </div>
<div> <div>
<label for="password">Passwort:</label> <label for="password">{{ fluent(key="password", lang=user.language)
}}:</label>
<input type="password" name="password"> <input type="password" name="password">
</div> </div>
<input type="submit" value="Einladen"> <input type="submit" value='{{ fluent(key="invite-user", lang=user.language)
}}'>
</form> </form>
<h2>&nbsp;</h2> <h2>&nbsp;</h2>
</div> </div>

View File

@ -4,15 +4,18 @@
<div class="center"> <div class="center">
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="title">Beschreibung:</label> <label for="title">{{ fluent(key="link-description", lang=user.language)
}}:</label>
<input type="text" name="title"> <input type="text" name="title">
</div> </div>
<div> <div>
<label for="target">Ziel:</label> <label for="target">{{ fluent(key="link-target", lang=user.language)
}}:</label>
<input type="text" name="target"> <input type="text" name="target">
</div> </div>
<div> <div>
<label for="code">Code:</label> <label for="code">{{ fluent(key="link-code", lang=user.language)
}}:</label>
<input type="text" name="code"> <input type="text" name="code">
</div> </div>
<input type="submit" value="Submit"> <input type="submit" value="Submit">

View File

@ -5,24 +5,29 @@
<h1>{{ link.title }}</h1> <h1>{{ link.title }}</h1>
<table> <table>
<tr> <tr>
<td>Beschreibung:</td> <td>{{ fluent(key="link-description", lang=user.language)
}}:</td>
<td>{{ link.title }}</td> <td>{{ link.title }}</td>
</tr> </tr>
<tr> <tr>
<td>Code:</td> <td>{{ fluent(key="link-code", lang=user.language)
}}:</td>
<td>{{ link.code }}</td> <td>{{ link.code }}</td>
</tr> </tr>
<tr> <tr>
<td>Kurzlink:</td> <td>{{ fluent(key="shortlink", lang=user.language)
}}:</td>
<td><a href="{{ protocol }}://{{ host }}/{{ link.code }}">{{ protocol }}://{{ host }}/{{ link.code }}</a> <td><a href="{{ protocol }}://{{ host }}/{{ link.code }}">{{ protocol }}://{{ host }}/{{ link.code }}</a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Ziel:</td> <td>{{ fluent(key="link-target", lang=user.language)
}}:</td>
<td>{{ link.target }}</td> <td>{{ link.target }}</td>
</tr> </tr>
<tr> <tr>
<td>QR-Code</td> <td>{{ fluent(key="qr-code", lang=user.language)
}}</td>
<td><a href="/admin/download/png/{{ link.code }}" download="{{ link.title | slugify }}.png"> <td><a href="/admin/download/png/{{ link.code }}" download="{{ link.title | slugify }}.png">
{{ qr | trim_start_matches(pat= {{ qr | trim_start_matches(pat=
'.*?>') '.*?>')
@ -33,7 +38,8 @@
</table> </table>
{% if user.role == 2 or user.id == link.author %} {% if user.role == 2 or user.id == link.author %}
<div class="actions"> <div class="actions">
<a class="button" href="/admin/edit/link/{{ link.code }}">Editieren</a> <a class="button" href="/admin/edit/link/{{ link.code }}">{{ fluent(key="edit-link", lang=user.language)
}}</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -2,31 +2,39 @@
{% block admin %} {% block admin %}
<div class="center"> <div class="center">
<h1>Profil von {{viewed_user.username}}</h1> <h1>{{ fluent(key="user-headline", lang=user.language, username=user.username) }}</h1>
<form action="" method="POST"> <form action="" method="POST">
<div> <div>
<label for="username">Benutzername:</label> <label for="username">{{ fluent(key="username", lang=user.language)
}}:</label>
<input type="text" name="username" value="{{ viewed_user.username }}" readonly> <input type="text" name="username" value="{{ viewed_user.username }}" readonly>
</div> </div>
<div> <div>
<label for="email">E-mail:</label> <label for="email">{{ fluent(key="email", lang=user.language)
}}:</label>
<input type="email" name="email" value="{{ viewed_user.email }}" readonly> <input type="email" name="email" value="{{ viewed_user.email }}" readonly>
</div> </div>
{% if user.role == 2 or user.id == viewed_user.id %} {% if user.role == 2 or user.id == viewed_user.id %}
<div> <div>
<label for="password">Passwort:</label> <label for="password">{{ fluent(key="password", lang=user.language)
}}:</label>
<input type="password" name="password" value="verschlüsselt" readonly> <input type="password" name="password" value="verschlüsselt" readonly>
</div> </div>
{% endif %} {% endif %}
</form> </form>
{% if user.role == 2 or user.id == viewed_user.id %} {% if user.role == 2 or user.id == viewed_user.id %}
<div class="actions"> <div class="actions">
<a class="button" href="/admin/edit/profile/{{ viewed_user.id }}">Editieren</a> <a class="button" href="/admin/edit/profile/{{ viewed_user.id }}">{{ fluent(key="edit-user", lang=user.language)
}}</a>
{% if user.role == 2 and viewed_user.role == 1 %} {% if user.role == 2 and viewed_user.role == 1 %}
<a class="button" href="/admin/edit/set_admin/{{ viewed_user.id }}">Zum Admin machen</a> <a class="button" href="/admin/edit/set_admin/{{ viewed_user.id }}">{{ fluent(key="make-user-admin",
lang=user.language)
}}</a>
{% endif %} {% endif %}
{% if user.role == 2 and viewed_user.role == 2 %} {% if user.role == 2 and viewed_user.role == 2 and not user.id == viewed_user.id %}
<a class="button" href="/admin/edit/set_admin/{{ viewed_user.id }}">Zum Normalo machen</a> <a class="button" href="/admin/edit/set_admin/{{ viewed_user.id }}">{{ fluent(key="make-user-regular",
lang=user.language)
}}</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}