summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Reeder <colin@vpzom.click>2020-07-21 17:29:37 -0600
committerColin Reeder <colin@vpzom.click>2020-07-21 17:29:47 -0600
commit207d06ae4118bf42828db2300cee74c0a42237ba (patch)
tree02ab7a29b77914a93356e3142673c11697f7791f
parentff37f13ba8d0cccd8c06db9b52001cd6388e029e (diff)
Initial work on translation support
-rw-r--r--Cargo.lock170
-rw-r--r--Cargo.toml4
-rw-r--r--res/lang/en.flt66
-rw-r--r--res/lang/eo.flt66
-rw-r--r--src/components/mod.rs52
-rw-r--r--src/main.rs85
-rw-r--r--src/routes/communities.rs140
-rw-r--r--src/routes/mod.rs257
-rw-r--r--src/routes/posts.rs57
9 files changed, 713 insertions, 184 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 771e7b6..ca76695 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -34,6 +34,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
name = "bytes"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -100,6 +106,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
+name = "fluent"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3b6132d1377d8776409a337c6851d342aee4e85277c96ecd2755c4e0efde1d"
+dependencies = [
+ "fluent-bundle",
+ "unic-langid",
+]
+
+[[package]]
+name = "fluent-bundle"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a094d494ab2ed06077e9a95f4e47f446c376de95f6c93045dd88c499bfcd70"
+dependencies = [
+ "fluent-langneg",
+ "fluent-syntax",
+ "intl-memoizer",
+ "intl_pluralrules",
+ "rental",
+ "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.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba"
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -192,6 +238,15 @@ dependencies = [
]
[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -237,10 +292,13 @@ dependencies = [
"ammonia",
"chrono",
"fallible-iterator",
+ "fluent",
+ "fluent-langneg",
"ginger",
"http",
"hyper",
"hyper-tls",
+ "lazy_static",
"render",
"serde",
"serde_derive",
@@ -249,6 +307,7 @@ dependencies = [
"timeago",
"tokio",
"trout",
+ "unic-langid",
"urlencoding",
]
@@ -351,6 +410,26 @@ dependencies = [
]
[[package]]
+name = "intl-memoizer"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0ed58ba6089d49f8a9a7d5e16fc9b9e2019cdf40ef270f3d465fa244d9630b"
+dependencies = [
+ "type-map",
+ "unic-langid",
+]
+
+[[package]]
+name = "intl_pluralrules"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c271cdb1f12a9feb3a017619c3ee681f971f270f6757341d6abe1f9f7a98bc3"
+dependencies = [
+ "tinystr",
+ "unic-langid",
+]
+
+[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -736,6 +815,12 @@ dependencies = [
]
[[package]]
+name = "proc-macro-hack"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
+
+[[package]]
name = "proc-macro2"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -954,6 +1039,27 @@ dependencies = [
]
[[package]]
+name = "rental"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f"
+dependencies = [
+ "rental-impl",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "rental-impl"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1069,6 +1175,12 @@ dependencies = [
]
[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
name = "string_cache"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1181,6 +1293,12 @@ dependencies = [
]
[[package]]
+name = "tinystr"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bac79c4b51eda1b090b1edebfb667821bbb51f713855164dc7cec2cb8ac2ba3"
+
+[[package]]
name = "tokio"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1256,6 +1374,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
[[package]]
+name = "type-map"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717"
+dependencies = [
+ "fxhash",
+]
+
+[[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",
+ "syn",
+ "unic-langid-impl",
+]
+
+[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index f73c340..161636e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,7 @@ urlencoding = "1.1.1"
http = "0.2.1"
chrono = "0.4.13"
timeago = "0.2.1"
+fluent-langneg = "0.13.0"
+fluent = "0.12.0"
+lazy_static = "1.4.0"
+unic-langid = { version = "0.9.0", features = ["macros"] }
diff --git a/res/lang/en.flt b/res/lang/en.flt
new file mode 100644
index 0000000..51b9d78
--- /dev/null
+++ b/res/lang/en.flt
@@ -0,0 +1,66 @@
+about = About
+about_title = About this instance
+about_what_is = What is lotide?
+about_text1 = lotide is an attempt to build a federated forum. Users can create communities to share links and text posts and discuss them with other users, include those registered on other servers through
+about_text2 = For more information or to view the source code, check out the
+about_sourcehut = SourceHut page
+about_versions = This instance is running hitide { $hitide_version } on { $backend_name } { $backend_version }.
+add_by_remote_id = Add by ID:
+all = All
+all_title = The Whole Known Network
+by = by
+comment = Comment
+comment_delete_title = Delete Comment
+comment_delete_question = Delete this comment?
+comment_submit = Post Comment
+communities = Communities
+community_create = Create Community
+community_create_submit = Create
+community_edit = Customize Community
+community_edit_link = Customize
+community_remote_note = This is a remote community, information on this page may be incomplete.
+delete = delete
+delete_yes = Yes, delete
+description = Description
+edit = Edit
+fetch = Fetch
+follow = Follow
+follow_request_sent = Follow request sent!
+follow_undo = Unfollow
+home_follow_prompt1 = Why not
+home_follow_prompt2 = follow some communities?
+like = Like
+like_undo = Unlike
+local = Local
+login = Login
+login_signup_link = create a new account
+lookup_nothing = Nothing found.
+lookup_title = Lookup
+name_prompt = Name:
+no_cancel = No, cancel
+nothing = Looks like there's nothing here.
+nothing_yet = Looks like there's nothing here (yet!).
+on = on
+or_start = Or
+password_prompt = Password:
+post_delete_question = Delete this post?
+post_delete_title = Delete Post
+post_new = New Post
+register = Register
+remote = Remote
+reply = reply
+reply_submit = Reply
+submit = Submit
+submitted = Submitted
+text_with_markdown = Text (markdown supported)
+title = Title
+to = to
+url = URL
+user_edit_description_prompt = Profile Description:
+user_edit_not_you = You can only edit your own profile.
+user_edit_submit = Save
+user_edit_title = Edit Profile
+user_remote_note = This is a remote user, information on this page may be incomplete.
+username_prompt = Username:
+view_at_source = View at Source
+view_more_comments = View More Comments
diff --git a/res/lang/eo.flt b/res/lang/eo.flt
new file mode 100644
index 0000000..fe01c53
--- /dev/null
+++ b/res/lang/eo.flt
@@ -0,0 +1,66 @@
+about = Pri
+about_title = Pri ĉi tiu servilo
+about_what_is = Kio estas lotide?
+about_text1 = lotide estas provo konstrui federacian forumon. Uzantoj povas krei komunumojn por disdoni ligilojn kaj tekstpoŝtojn kaj diskuti ilin kun aliaj uzantoj, inkluzive de tiuj en aliaj serviloj per
+about_text2 = Por pli da informo aŭ vidi la fontkodon, kontrolu la
+about_sourcehut = SourceHut paĝon
+about_versions = Ĉi tiu servilo uzas hitide { $hitide_version } kun { $backend_name } { $backend_version }.
+add_by_remote_id = Aldoni per ID:
+all = Ĉiuj
+all_title = La Tuta Konata Reto
+by = de
+comment = Komento
+comment_delete_title = Forigi Komenton
+comment_delete_question = Ĉu vi volas forigi ĉi tiun komenton?
+comment_submit = Afiŝi Komenton
+communities = Komunumoj
+community_create = Krei Komunumon
+community_create_submit = Krei
+community_edit = Agordi Komunumon
+community_edit_link = Agordi
+community_remote_note = Ĉi tiu estas fora komunumo, informo en ĉi tiu paĝo eble neplenas.
+delete = forigi
+delete_yes = Jes, forigi
+description = Priskribo
+edit = Redakti
+fetch = Alporti
+follow = Aboni
+follow_request_sent = Abonado peto senditas!
+follow_undo = Ne plu aboni
+home_follow_prompt1 = Kial ne
+home_follow_prompt2 = aboni iujn komunumojn?
+like = Ŝati
+like_undo = Ne plu ŝati
+local = Loka
+login = Ensaluti
+login_signup_link = krei novan konton
+lookup_nothing = Nenio troveblas.
+lookup_title = Serĉi
+name_prompt = Nomo:
+no_cancel = Ne, nuligi
+nothing = Ŝajnas, ke estas nenio ĉi tie.
+nothing_yet = Ŝajnas, ke estas nenio ĉi tie (ĝis nun!).
+on = sur
+or_start = Aŭ
+password_prompt = Pasvorto:
+post_delete_question = Ĉu vi volas forigi ĉi tiun poŝton?
+post_delete_title = Forigi Poŝton
+post_new = Nova Poŝto
+register = Registriĝi
+remote = Fora
+reply = respondi
+reply_submit = Respondi
+submit = Sendi
+submitted = Afiŝita
+text_with_markdown = Teksto (markdown estas permesita)
+title = Titolo
+to = al
+url = URL
+user_edit_description_prompt = Priskribo de Profilo
+user_edit_not_you = Vi nur rajtas redakti vian propran profilon.
+user_edit_submit = Konservi
+user_edit_title = Redakti Profilon
+user_remote_note = Ĉi tiu estas fora uzanto, informo en ĉi tiu paĝo eble neplenas.
+username_prompt = Uzantnomo:
+view_at_source = Vidi ĉe Fonto
+view_more_comments = Vidi Pli da Komentoj
diff --git a/src/components/mod.rs b/src/components/mod.rs
index f6b5785..b43b43b 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -9,9 +9,10 @@ use crate::util::{abbreviate_link, author_is_me};
use crate::PageBaseData;
#[render::component]
-pub fn Comment<'comment, 'base_data>(
- comment: &'comment RespPostCommentInfo<'comment>,
- base_data: &'base_data PageBaseData,
+pub fn Comment<'a>(
+ comment: &'a RespPostCommentInfo<'a>,
+ base_data: &'a PageBaseData,
+ lang: &'a crate::Translator,
) {
render::rsx! {
<li>
@@ -30,18 +31,18 @@ pub fn Comment<'comment, 'base_data>(
if comment.your_vote.is_some() {
render::rsx! {
<form method={"POST"} action={format!("/comments/{}/unlike", comment.id)}>
- <button type={"submit"}>{"Unlike"}</button>
+ <button type={"submit"}>{lang.tr("like_undo", None)}</button>
</form>
}
} else {
render::rsx! {
<form method={"POST"} action={format!("/comments/{}/like", comment.id)}>
- <button type={"submit"}>{"Like"}</button>
+ <button type={"submit"}>{lang.tr("like", None)}</button>
</form>
}
}
}
- <a href={format!("/comments/{}", comment.id)}>{"reply"}</a>
+ <a href={format!("/comments/{}", comment.id)}>{lang.tr("reply", None)}</a>
</>
})
} else {
@@ -51,7 +52,7 @@ pub fn Comment<'comment, 'base_data>(
{
if author_is_me(&comment.author, &base_data.login) {
Some(render::rsx! {
- <a href={format!("/comments/{}/delete", comment.id)}>{"delete"}</a>
+ <a href={format!("/comments/{}/delete", comment.id)}>{lang.tr("delete", None)}</a>
})
} else {
None
@@ -66,7 +67,7 @@ pub fn Comment<'comment, 'base_data>(
{
replies.iter().map(|reply| {
render::rsx! {
- <Comment comment={reply} base_data />
+ <Comment comment={reply} base_data lang />
}
})
.collect::<Vec<_>>()
@@ -80,7 +81,7 @@ pub fn Comment<'comment, 'base_data>(
{
if comment.replies.is_none() && comment.has_replies {
Some(render::rsx! {
- <ul><li><a href={format!("/comments/{}", comment.id)}>{"-> View More Comments"}</a></li></ul>
+ <ul><li><a href={format!("/comments/{}", comment.id)}>{"-> "}{lang.tr("view_more_comments", None)}</a></li></ul>
})
} else {
None
@@ -174,6 +175,7 @@ impl<'a, T: HavingContent + 'a> render::Render for Content<'a, T> {
#[render::component]
pub fn HTPage<'a, Children: render::Render>(
base_data: &'a PageBaseData,
+ lang: &'a crate::Translator,
title: &'a str,
children: Children,
) {
@@ -190,19 +192,19 @@ pub fn HTPage<'a, Children: render::Render>(
<header class={"mainHeader"}>
<div class={"left actionList"}>
<a href={"/"} class={"siteName"}>{"lotide"}</a>
- <a href={"/all"}>{"All"}</a>
- <a href={"/communities"}>{"Communities"}</a>
- <a href={"/about"}>{"About"}</a>
+ <a href={"/all"}>{lang.tr("all", None)}</a>
+ <a href={"/communities"}>{lang.tr("communities", None)}</a>
+ <a href={"/about"}>{lang.tr("about", None)}</a>
</div>
<div class={"right actionList"}>
{
match &base_data.login {
Some(login) => Some(render::rsx! {
- <a href={format!("/users/{}", login.user.id)}>{"👤︎"}</a>
+ <a href={format!("/users/{}", login.user.id)}>{Cow::Borrowed("👤︎")}</a>
}),
None => {
Some(render::rsx! {
- <a href={"/login"}>{"Login"}</a>
+ <a href={"/login"}>{lang.tr("login", None)}</a>
})
}
}
@@ -217,7 +219,12 @@ pub fn HTPage<'a, Children: render::Render>(
}
#[render::component]
-pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool, no_user: bool) {
+pub fn PostItem<'a>(
+ post: &'a RespPostListPost<'a>,
+ in_community: bool,
+ no_user: bool,
+ lang: &'a crate::Translator,
+) {
render::rsx! {
<li>
<a href={format!("/posts/{}", post.as_ref().id)}>
@@ -236,14 +243,14 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
}
}
<br />
- {"Submitted"}
+ {lang.tr("submitted", None)}
{
if no_user {
None
} else {
Some(render::rsx! {
<>
- {" by "}<UserLink user={post.author.as_ref()} />
+ {" "}{lang.tr("by", None)}{" "}<UserLink user={post.author.as_ref()} />
</>
})
}
@@ -251,7 +258,7 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
{
if !in_community {
Some(render::rsx! {
- <>{" to "}<CommunityLink community={&post.community} /></>
+ <>{" "}{lang.tr("to", None)}{" "}<CommunityLink community={&post.community} /></>
})
} else {
None
@@ -262,21 +269,24 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
}
pub struct ThingItem<'a> {
+ pub lang: &'a crate::Translator,
pub thing: &'a RespThingInfo<'a>,
}
impl<'a> render::Render for ThingItem<'a> {
fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
+ let lang = self.lang;
+
match self.thing {
RespThingInfo::Post(post) => {
- (PostItem { post, in_community: false, no_user: true }).render_into(writer)
+ (PostItem { post, in_community: false, no_user: true, lang: self.lang }).render_into(writer)
},
RespThingInfo::Comment(comment) => {
(render::rsx! {
<li>
<small>
- <a href={format!("/comments/{}", comment.id)}>{"Comment"}</a>
- {" on "}<a href={format!("/posts/{}", comment.post.id)}>{comment.post.title.as_ref()}</a>{":"}
+ <a href={format!("/comments/{}", comment.id)}>{lang.tr("comment", None)}</a>
+ {" "}{lang.tr("on", None)}{" "}<a href={format!("/posts/{}", comment.post.id)}>{comment.post.title.as_ref()}</a>{":"}
</small>
<Content src={comment} />
</li>
diff --git a/src/main.rs b/src/main.rs
index 2e23682..d513f20 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,8 @@
#![allow(unused_braces)]
use crate::resp_types::RespLoginInfo;
+use std::borrow::Cow;
+use std::collections::HashMap;
use std::sync::Arc;
use trout::hyper::RoutingFailureExtHyper;
@@ -54,6 +56,89 @@ pub fn simple_response(
res
}
+lazy_static::lazy_static! {
+ static ref LANG_MAP: HashMap<unic_langid::LanguageIdentifier, fluent::FluentResource> = {
+ let mut result = HashMap::new();
+
+ result.insert(unic_langid::langid!("en"), fluent::FluentResource::try_new(include_str!("../res/lang/en.flt").to_owned()).expect("Failed to parse translation"));
+ result.insert(unic_langid::langid!("eo"), fluent::FluentResource::try_new(include_str!("../res/lang/eo.flt").to_owned()).expect("Failed to parse translation"));
+
+ result
+ };
+
+ static ref LANGS: Vec<unic_langid::LanguageIdentifier> = {
+ LANG_MAP.keys().cloned().collect()
+ };
+}
+
+pub struct Translator {
+ bundle: fluent::concurrent::FluentBundle<&'static fluent::FluentResource>,
+}
+impl Translator {
+ pub fn tr<'a>(&'a self, key: &str, args: Option<&'a fluent::FluentArgs>) -> Cow<'a, str> {
+ let mut errors = Vec::with_capacity(0);
+ let out = self.bundle.format_pattern(
+ self.bundle
+ .get_message(key)
+ .expect("Missing message in translation")
+ .value
+ .expect("Missing value for translation key"),
+ args,
+ &mut errors,
+ );
+ if !errors.is_empty() {
+ eprintln!("Errors in translation: {:?}", errors);
+ }
+
+ out
+ }
+}
+impl std::fmt::Debug for Translator {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Translator")
+ }
+}
+
+pub fn get_lang_for_headers(headers: &hyper::header::HeaderMap) -> Translator {
+ let default = unic_langid::langid!("en");
+ let languages = match headers
+ .get(hyper::header::ACCEPT_LANGUAGE)
+ .and_then(|x| x.to_str().ok())
+ {
+ Some(accept_language) => {
+ let requested = fluent_langneg::accepted_languages::parse(accept_language);
+ fluent_langneg::negotiate_languages(
+ &requested,
+ &LANGS,
+ Some(&default),
+ fluent_langneg::NegotiationStrategy::Filtering,
+ )
+ }
+ None => vec![&default],
+ };
+
+ let mut bundle = fluent::concurrent::FluentBundle::new(languages.iter().map(|x| *x));
+ for lang in languages {
+ if let Err(errors) = bundle.add_resource(&LANG_MAP[lang]) {
+ for err in errors {
+ match err {
+ fluent::FluentError::Overriding { .. } => {}
+ _ => {
+ eprintln!("Failed to add language resource: {:?}", err);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ Translator { bundle }
+}
+
+pub fn get_lang_for_req(req: &hyper::Request<hyper::Body>) -> Translator {
+ get_lang_for_headers(req.headers())
+}
+
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend_host = std::env::var("BACKEND_HOST").expect("Missing BACKEND_HOST");
diff --git a/src/routes/communities.rs b/src/routes/communities.rs
index e4b1f13..442b958 100644
--- a/src/routes/communities.rs
+++ b/src/routes/communities.rs
@@ -3,8 +3,8 @@ use crate::resp_types::{
RespCommunityInfoMaybeYour, RespMinimalCommunityInfo, RespPostListPost, RespYourFollow,
};
use crate::routes::{
- fetch_base_data, get_cookie_map, get_cookie_map_for_headers, get_cookie_map_for_req,
- html_response, res_to_error, with_auth, CookieMap,
+ fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
+ res_to_error, CookieMap,
};
use serde_derive::Deserialize;
use std::collections::HashMap;
@@ -15,8 +15,10 @@ async fn page_communities(
ctx: Arc<crate::RouteContext>,
req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
+ let lang = crate::get_lang_for_req(&req);
let cookies = get_cookie_map_for_req(&req)?;
- let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
+ let base_data =
+ fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;
let api_res = res_to_error(
ctx.http_client
@@ -30,14 +32,16 @@ async fn page_communities(
let api_res = hyper::body::to_bytes(api_res.into_body()).await?;
let communities: Vec<RespMinimalCommunityInfo> = serde_json::from_slice(&api_res)?;
+ let title = lang.tr("communities", None);
+
Ok(html_response(render::html! {
- <HTPage base_data={&base_data} title={"Communities"}>
- <h1>{"Communities"}</h1>
+ <HTPage base_data={&base_data} lang={&lang} title={&title}>
+ <h1>{title.as_ref()}</h1>
<div>
- <h2>{"Local"}</h2>
+ <h2>{lang.tr("local", None)}</h2>
{
if base_data.login.is_some() {
- Some(render::rsx! { <a href={"/new_community"}>{"Create Community"}</a> })
+ Some(render::rsx! { <a href={"/new_community"}>{lang.tr("community_create", None)}</a> })
} else {
None
}
@@ -56,14 +60,14 @@ async fn page_communities(
</ul>
</div>
<div>
- <h2>{"Remote"}</h2>
+ <h2>{lang.tr("remote", None)}</h2>
<form method={"GET"} action={"/lookup"}>
<label>
- {"Add by ID: "}
+ {lang.tr("add_by_remote_id", None)}{" "}
<input r#type={"text"} name={"query"} placeholder={"group@example.com"} />
</label>
{" "}
- <button r#type={"submit"}>{"Fetch"}</button>
+ <button r#type={"submit"}>{lang.tr("fetch", None)}</button>
</form>
<ul>
{
@@ -89,15 +93,17 @@ async fn page_community(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
let (community_id,) = params;
+ let lang = crate::get_lang_for_req(&req);
let cookies = get_cookie_map_for_req(&req)?;
// TODO parallelize requests
- let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
+ let base_data =
+ fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;
let community_info_api_res = res_to_error(
ctx.http_client
- .request(with_auth(
+ .request(for_client(
hyper::Request::get(format!(
"{}/api/unstable/communities/{}{}",
ctx.backend_host,
@@ -109,6 +115,7 @@ async fn page_community(
},
))
.body(Default::default())?,
+ req.headers(),
&cookies,
)?)
.await?,
@@ -121,12 +128,13 @@ async fn page_community(
let posts_api_res = res_to_error(
ctx.http_client
- .request(with_auth(
+ .request(for_client(
hyper::Request::get(format!(
"{}/api/unstable/communities/{}/posts",
ctx.backend_host, community_id
))
.body(Default::default())?,
+ req.headers(),
&cookies,
)?)
.await?,
@@ -141,7 +149,7 @@ async fn page_community(
let title = community_info.as_ref().name.as_ref();
Ok(html_response(render::html! {
- <HTPage base_data={&base_data} title>
+ <HTPage base_data={&base_data} lang={&lang} title>
<div class={"communitySidebar"}>
<h2>{title}</h2>
<div><em>{format!("@{}@{}", community_info.as_ref().name, community_info.as_ref().host)}</em></div>
@@ -151,8 +159,9 @@ async fn page_community(
} else if let Some(remote_url) = &community_info.as_ref().remote_url {
Some(render::rsx! {
<div class={"infoBox"}>
- {"This is a remote community, information on this page may be incomplete. "}
- <a href={remote_url.as_ref()}>{"View at Source ↗"}</a>
+ {lang.tr("community_remote_note", None)}
+ {" "}
+ <a href={remote_url.as_ref()}>{lang.tr("view_at_source", None)}{" ↗"}</a>
</div>
})
} else {
@@ -166,21 +175,21 @@ async fn page_community(
Some(RespYourFollow { accepted: true }) => {
render::rsx! {
<form method={"POST"} action={format!("/communities/{}/unfollow", community_id)}>
- <button type={"submit"}>{"Unfollow"}</button>
+ <button type={"submit"}>{lang.tr("follow_undo", None)}</button>
</form>
}
},
Some(RespYourFollow { accepted: false }) => {
render::rsx! {
<form>
- <button disabled={""}>{"Follow request sent!"}</button>
+ <button disabled={""}>{lang.tr("follow_request_sent", None)}</button>
</form>
}
},
None => {
render::rsx! {
<form method={"POST"} action={format!("/communities/{}/follow", community_id)}>
- <button type={"submit"}>{"Follow"}</button>
+ <button type={"submit"}>{lang.tr("follow", None)}</button>
</form>
}
}
@@ -191,13 +200,13 @@ async fn page_community(
}
</p>
<p>
- <a href={&new_post_url}>{"New Post"}</a>
+ <a href={&new_post_url}>{lang.tr("post_new", None)}</a>
</p>
{
if community_info.you_are_moderator == Some(true) {
Some(render::rsx! {
<p>
- <a href={format!("/communities/{}/edit", community_id)}>{"Customize"}</a>
+ <a href={format!("/communities/{}/edit", community_id)}>{lang.tr("community_edit_link", None)}</a>
</p>
})
} else {
@@ -208,14 +217,14 @@ async fn page_community(
</div>
{
if posts.is_empty() {
- Some(render::rsx! { <p>{"Looks like there's nothing here."}</p> })
+ Some(render::rsx! { <p>{lang.tr("nothing", None)}</p> })
} else {
None
}
}
<ul>
{posts.iter().map(|post| {
- PostItem { post, in_community: true, no_user: false }
+ PostItem { post, in_community: true, no_user: false, lang: &lang }
}).collect::<Vec<_>>()}
</ul>
</HTPage>
@@ -231,26 +240,29 @@ async fn page_community_edit(
let cookies = get_cookie_map_for_req(&req)?;
- page_community_edit_inner(community_id, &cookies, ctx, None, None).await
+ page_community_edit_inner(community_id, req.headers(), &cookies, ctx, None, None).await
}
async fn page_community_edit_inner(
community_id: i64,
+ headers: &hyper::header::HeaderMap,
cookies: &CookieMap<'_>,
ctx: Arc<crate::RouteContext>,
display_error: Option<String>,
prev_values: Option<&HashMap<&str, serde_json::Value>>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
- let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
+ let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;
+ let lang = crate::get_lang_for_headers(headers);
let community_info_api_res = res_to_error(
ctx.http_client
- .request(with_auth(
+ .request(for_client(
hyper::Request::get(format!(
"{}/api/unstable/communities/{}",
ctx.backend_host, community_id,
))
.body(Default::default())?,
+ he