Adding support for internationalization / i18n (#189)

* Still not working

* Starting to work on internationalization

* Main done.

* i18n translations first pass.

* Localization testing mostly done.

* Second front end pass.

* Added a few more translations.

* Adding back end translations.
This commit is contained in:
Dessalines 2019-08-09 17:14:43 -07:00 committed by GitHub
parent 5a1e8aa645
commit 536c3f4915
39 changed files with 862 additions and 410 deletions

View file

@ -53,7 +53,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -62,12 +62,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
let content_slurs_removed = remove_slurs(&data.content.to_owned()); let content_slurs_removed = remove_slurs(&data.content.to_owned());
@ -86,7 +86,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let inserted_comment = match Comment::create(&conn, &comment_form) { let inserted_comment = match Comment::create(&conn, &comment_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't create Comment"))? return Err(APIError::err(&self.op, "couldnt_create_comment"))?
} }
}; };
@ -101,7 +101,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let _inserted_like = match CommentLike::like(&conn, &like_form) { let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like, Ok(like) => like,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't like comment."))? return Err(APIError::err(&self.op, ""))?
} }
}; };
@ -124,7 +124,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -153,17 +153,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
); );
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "Not allowed to edit comment."))? return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
} }
@ -184,7 +184,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update Comment"))? return Err(APIError::err(&self.op, "couldnt_update_comment"))?
} }
}; };
@ -220,7 +220,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -235,14 +235,14 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
match CommentSaved::save(&conn, &comment_saved_form) { match CommentSaved::save(&conn, &comment_saved_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldnt do comment save"))? return Err(APIError::err(&self.op, "couldnt_save_comment"))?
} }
}; };
} else { } else {
match CommentSaved::unsave(&conn, &comment_saved_form) { match CommentSaved::unsave(&conn, &comment_saved_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldnt do comment save"))? return Err(APIError::err(&self.op, "couldnt_save_comment"))?
} }
}; };
} }
@ -266,7 +266,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -275,12 +275,12 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
@ -299,7 +299,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let _inserted_like = match CommentLike::like(&conn, &like_form) { let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like, Ok(like) => like,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't like comment."))? return Err(APIError::err(&self.op, "couldnt_like_comment"))?
} }
}; };
} }

View file

@ -135,14 +135,14 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
let community_view = match CommunityView::read(&conn, community_id, user_id) { let community_view = match CommunityView::read(&conn, community_id, user_id) {
Ok(community) => community, Ok(community) => community,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't find Community"))? return Err(APIError::err(&self.op, "couldnt_find_community"))?
} }
}; };
let moderators = match CommunityModeratorView::for_community(&conn, community_id) { let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't find Community"))? return Err(APIError::err(&self.op, "couldnt_find_community"))?
} }
}; };
@ -168,21 +168,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
if has_slurs(&data.name) || if has_slurs(&data.name) ||
has_slurs(&data.title) || has_slurs(&data.title) ||
(data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let user_id = claims.id; let user_id = claims.id;
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
// When you create a community, make sure the user becomes a moderator and a follower // When you create a community, make sure the user becomes a moderator and a follower
@ -200,7 +200,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let inserted_community = match Community::create(&conn, &community_form) { let inserted_community = match Community::create(&conn, &community_form) {
Ok(community) => community, Ok(community) => community,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community already exists."))? return Err(APIError::err(&self.op, "community_already_exists"))?
} }
}; };
@ -212,7 +212,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community moderator already exists."))? return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
} }
}; };
@ -224,7 +224,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community follower already exists."))? return Err(APIError::err(&self.op, "community_follower_already_exists"))?
} }
}; };
@ -244,7 +244,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let data: &EditCommunity = &self.data; let data: &EditCommunity = &self.data;
if has_slurs(&data.name) || has_slurs(&data.title) { if has_slurs(&data.name) || has_slurs(&data.title) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let conn = establish_connection(); let conn = establish_connection();
@ -252,7 +252,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -260,7 +260,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
// Verify its a mod // Verify its a mod
@ -280,7 +280,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
.collect() .collect()
); );
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "Not allowed to edit community"))? return Err(APIError::err(&self.op, "no_community_edit_allowed"))?
} }
let community_form = CommunityForm { let community_form = CommunityForm {
@ -297,7 +297,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) { let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
Ok(community) => community, Ok(community) => community,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update Community"))? return Err(APIError::err(&self.op, "couldnt_update_community"))?
} }
}; };
@ -369,7 +369,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -384,14 +384,14 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
match CommunityFollower::follow(&conn, &community_follower_form) { match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community follower already exists."))? return Err(APIError::err(&self.op, "community_follower_already_exists"))?
} }
}; };
} else { } else {
match CommunityFollower::ignore(&conn, &community_follower_form) { match CommunityFollower::ignore(&conn, &community_follower_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community follower already exists."))? return Err(APIError::err(&self.op, "community_follower_already_exists"))?
} }
}; };
} }
@ -416,7 +416,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -425,7 +425,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let communities: Vec<CommunityFollowerView> = match CommunityFollowerView::for_user(&conn, user_id) { let communities: Vec<CommunityFollowerView> = match CommunityFollowerView::for_user(&conn, user_id) {
Ok(communities) => communities, Ok(communities) => communities,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "System error, try logging out and back in."))? return Err(APIError::err(&self.op, "system_err_login"))?
} }
}; };
@ -448,7 +448,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -463,14 +463,14 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
match CommunityUserBan::ban(&conn, &community_user_ban_form) { match CommunityUserBan::ban(&conn, &community_user_ban_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community user ban already exists"))? return Err(APIError::err(&self.op, "community_user_already_banned"))?
} }
}; };
} else { } else {
match CommunityUserBan::unban(&conn, &community_user_ban_form) { match CommunityUserBan::unban(&conn, &community_user_ban_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community user ban already exists"))? return Err(APIError::err(&self.op, "community_user_already_banned"))?
} }
}; };
} }
@ -511,7 +511,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -526,14 +526,14 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
match CommunityModerator::join(&conn, &community_moderator_form) { match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community moderator already exists."))? return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
} }
}; };
} else { } else {
match CommunityModerator::leave(&conn, &community_moderator_form) { match CommunityModerator::leave(&conn, &community_moderator_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community moderator already exists."))? return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
} }
}; };
} }

View file

@ -94,25 +94,25 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
if has_slurs(&data.name) || if has_slurs(&data.name) ||
(data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) { (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let user_id = claims.id; let user_id = claims.id;
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
let post_form = PostForm { let post_form = PostForm {
@ -130,7 +130,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let inserted_post = match Post::create(&conn, &post_form) { let inserted_post = match Post::create(&conn, &post_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't create Post"))? return Err(APIError::err(&self.op, "couldnt_create_post"))?
} }
}; };
@ -145,7 +145,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let _inserted_like = match PostLike::like(&conn, &like_form) { let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like, Ok(like) => like,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't like post."))? return Err(APIError::err(&self.op, "couldnt_like_post"))?
} }
}; };
@ -153,7 +153,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) { let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't find Post"))? return Err(APIError::err(&self.op, "couldnt_find_post"))?
} }
}; };
@ -187,7 +187,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
let post_view = match PostView::read(&conn, data.id, user_id) { let post_view = match PostView::read(&conn, data.id, user_id) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't find Post"))? return Err(APIError::err(&self.op, "couldnt_find_post"))?
} }
}; };
@ -248,7 +248,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
data.limit) { data.limit) {
Ok(posts) => posts, Ok(posts) => posts,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't get posts"))? return Err(APIError::err(&self.op, "couldnt_get_posts"))?
} }
}; };
@ -270,7 +270,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -279,12 +279,12 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
let like_form = PostLikeForm { let like_form = PostLikeForm {
@ -302,7 +302,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let _inserted_like = match PostLike::like(&conn, &like_form) { let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like, Ok(like) => like,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't like post."))? return Err(APIError::err(&self.op, "couldnt_like_post"))?
} }
}; };
} }
@ -310,7 +310,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) { let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't find Post"))? return Err(APIError::err(&self.op, "couldnt_find_post"))?
} }
}; };
@ -329,7 +329,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let data: &EditPost = &self.data; let data: &EditPost = &self.data;
if has_slurs(&data.name) || if has_slurs(&data.name) ||
(data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) { (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let conn = establish_connection(); let conn = establish_connection();
@ -337,7 +337,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -360,17 +360,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
.collect() .collect()
); );
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "Not allowed to edit post."))? return Err(APIError::err(&self.op, "no_post_edit_allowed"))?
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
return Err(APIError::err(&self.op, "You have been banned from this community"))? return Err(APIError::err(&self.op, "community_ban"))?
} }
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "You have been banned from the site"))? return Err(APIError::err(&self.op, "site_ban"))?
} }
let post_form = PostForm { let post_form = PostForm {
@ -388,7 +388,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) { let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update Post"))? return Err(APIError::err(&self.op, "couldnt_update_post"))?
} }
}; };
@ -431,7 +431,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -446,14 +446,14 @@ impl Perform<PostResponse> for Oper<SavePost> {
match PostSaved::save(&conn, &post_saved_form) { match PostSaved::save(&conn, &post_saved_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldnt do post save"))? return Err(APIError::err(&self.op, "couldnt_save_post"))?
} }
}; };
} else { } else {
match PostSaved::unsave(&conn, &post_saved_form) { match PostSaved::unsave(&conn, &post_saved_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldnt do post save"))? return Err(APIError::err(&self.op, "couldnt_save_post"))?
} }
}; };
} }

View file

@ -144,20 +144,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
if has_slurs(&data.name) || if has_slurs(&data.name) ||
(data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin { if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "Not an admin."))? return Err(APIError::err(&self.op, "not_an_admin"))?
} }
let site_form = SiteForm { let site_form = SiteForm {
@ -170,7 +170,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
match Site::create(&conn, &site_form) { match Site::create(&conn, &site_form) {
Ok(site) => site, Ok(site) => site,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Site exists already"))? return Err(APIError::err(&self.op, "site_already_exists"))?
} }
}; };
@ -194,20 +194,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
if has_slurs(&data.name) || if has_slurs(&data.name) ||
(data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) { (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false { if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "Not an admin."))? return Err(APIError::err(&self.op, "not_an_admin"))?
} }
let found_site = Site::read(&conn, 1)?; let found_site = Site::read(&conn, 1)?;
@ -222,7 +222,7 @@ impl Perform<SiteResponse> for Oper<EditSite> {
match Site::update(&conn, 1, &site_form) { match Site::update(&conn, 1, &site_form) {
Ok(site) => site, Ok(site) => site,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update site."))? return Err(APIError::err(&self.op, "couldnt_update_site"))?
} }
}; };

View file

@ -102,13 +102,13 @@ impl Perform<LoginResponse> for Oper<Login> {
// Fetch that username / email // Fetch that username / email
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) { let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
Ok(user) => user, Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "Couldn't find that username or email"))? Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email"))?
}; };
// Verify the password // Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid { if !valid {
return Err(APIError::err(&self.op, "Password incorrect"))? return Err(APIError::err(&self.op, "password_incorrect"))?
} }
// Return the jwt // Return the jwt
@ -129,16 +129,16 @@ impl Perform<LoginResponse> for Oper<Register> {
// Make sure passwords match // Make sure passwords match
if &data.password != &data.password_verify { if &data.password != &data.password_verify {
return Err(APIError::err(&self.op, "Passwords do not match."))? return Err(APIError::err(&self.op, "passwords_dont_match"))?
} }
if has_slurs(&data.username) { if has_slurs(&data.username) {
return Err(APIError::err(&self.op, "No slurs"))? return Err(APIError::err(&self.op, "no_slurs"))?
} }
// Make sure there are no admins // Make sure there are no admins
if data.admin && UserView::admins(&conn)?.len() > 0 { if data.admin && UserView::admins(&conn)?.len() > 0 {
return Err(APIError::err(&self.op, "Sorry, there's already an admin."))? return Err(APIError::err(&self.op, "admin_already_created"))?
} }
// Register the new user // Register the new user
@ -157,7 +157,7 @@ impl Perform<LoginResponse> for Oper<Register> {
let inserted_user = match User_::register(&conn, &user_form) { let inserted_user = match User_::register(&conn, &user_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "User already exists."))? return Err(APIError::err(&self.op, "user_already_exists"))?
} }
}; };
@ -188,7 +188,7 @@ impl Perform<LoginResponse> for Oper<Register> {
let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community follower already exists."))? return Err(APIError::err(&self.op, "community_follower_already_exists"))?
} }
}; };
@ -202,7 +202,7 @@ impl Perform<LoginResponse> for Oper<Register> {
let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) { let _inserted_community_moderator = match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Community moderator already exists."))? return Err(APIError::err(&self.op, "community_moderator_already_exists"))?
} }
}; };
@ -321,7 +321,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -329,7 +329,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
// Make sure user is an admin // Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false { if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "Not an admin."))? return Err(APIError::err(&self.op, "not_an_admin"))?
} }
let read_user = User_::read(&conn, data.user_id)?; let read_user = User_::read(&conn, data.user_id)?;
@ -348,7 +348,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update user"))? return Err(APIError::err(&self.op, "couldnt_update_user"))?
} }
}; };
@ -380,7 +380,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -388,7 +388,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
// Make sure user is an admin // Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false { if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "Not an admin."))? return Err(APIError::err(&self.op, "not_an_admin"))?
} }
let read_user = User_::read(&conn, data.user_id)?; let read_user = User_::read(&conn, data.user_id)?;
@ -407,7 +407,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
match User_::update(&conn, data.user_id, &user_form) { match User_::update(&conn, data.user_id, &user_form) {
Ok(user) => user, Ok(user) => user,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update user"))? return Err(APIError::err(&self.op, "couldnt_update_user"))?
} }
}; };
@ -448,7 +448,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -476,7 +476,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Not logged in."))? return Err(APIError::err(&self.op, "not_logged_in"))?
} }
}; };
@ -499,7 +499,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) { let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => { Err(_e) => {
return Err(APIError::err(&self.op, "Couldn't update Comment"))? return Err(APIError::err(&self.op, "couldnt_update_comment"))?
} }
}; };
} }

View file

@ -23,7 +23,9 @@
"autosize": "^4.0.2", "autosize": "^4.0.2",
"classcat": "^1.1.3", "classcat": "^1.1.3",
"dotenv": "^6.1.0", "dotenv": "^6.1.0",
"i18next": "^17.0.9",
"inferno": "^7.0.1", "inferno": "^7.0.1",
"inferno-i18next": "nimbusec-oss/inferno-i18next",
"inferno-router": "^7.0.1", "inferno-router": "^7.0.1",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
@ -35,6 +37,7 @@
"ws": "^7.0.0" "ws": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/i18next": "^12.1.0",
"fuse-box": "^3.1.3", "fuse-box": "^3.1.3",
"ts-transform-classcat": "^0.0.2", "ts-transform-classcat": "^0.0.2",
"ts-transform-inferno": "^4.0.2", "ts-transform-inferno": "^4.0.2",

View file

@ -1,7 +1,10 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
import { capitalizeFirstLetter } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface CommentFormProps { interface CommentFormProps {
postId?: number; postId?: number;
@ -25,12 +28,13 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId, post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null, creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
}, },
buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply", buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
if (this.props.node) { if (this.props.node) {
@ -62,7 +66,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button> <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
{this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>} {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
</div> </div>
</div> </div>
</form> </form>
@ -84,7 +88,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
if (i.props.node) { if (i.props.node) {
i.props.onReplyCancel(); i.props.onReplyCancel();
} }
autosize.update(document.querySelector('textarea')); autosize.update(document.querySelector('textarea'));
} }

View file

@ -7,6 +7,8 @@ import * as moment from 'moment';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form'; import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
enum BanType {Community, Site}; enum BanType {Community, Site};
@ -74,10 +76,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link> <Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link>
</li> </li>
{this.isMod && {this.isMod &&
<li className="list-inline-item badge badge-light">mod</li> <li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li>
} }
{this.isAdmin && {this.isAdmin &&
<li className="list-inline-item badge badge-light">admin</li> <li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li>
} }
<li className="list-inline-item"> <li className="list-inline-item">
<span>( <span>(
@ -97,24 +99,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />} {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
{!this.state.showEdit && !this.state.collapsed && {!this.state.showEdit && !this.state.collapsed &&
<div> <div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.deleted ? '*deleted*' : node.comment.content)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? `*${i18n.t('removed')}*` : node.comment.deleted ? `*${i18n.t('deleted')}*` : node.comment.content)} />
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{UserService.Instance.user && !this.props.viewOnly && {UserService.Instance.user && !this.props.viewOnly &&
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span> <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? 'unsave' : 'save'}</span> <span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}</span>
</li> </li>
{this.myComment && {this.myComment &&
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
{!this.props.node.comment.deleted ? 'delete' : 'restore'} {!this.props.node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
</span> </span>
</li> </li>
</> </>
@ -123,8 +125,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.canMod && {this.canMod &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.node.comment.removed ? {!this.props.node.comment.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
} }
</li> </li>
} }
@ -134,14 +136,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!this.isMod && {!this.isMod &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.node.comment.banned_from_community ? {!this.props.node.comment.banned_from_community ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> : <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span> <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
} }
</li> </li>
} }
{!this.props.node.comment.banned_from_community && {!this.props.node.comment.banned_from_community &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span> <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
</li> </li>
} }
</> </>
@ -152,14 +154,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!this.isAdmin && {!this.isAdmin &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.node.comment.banned ? {!this.props.node.comment.banned ?
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> : <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban from site</span> <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
} }
</li> </li>
} }
{!this.props.node.comment.banned && {!this.props.node.comment.banned &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{`${this.isAdmin ? 'remove' : 'appoint'} as admin`}</span> <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
</li> </li>
} }
</> </>
@ -167,11 +169,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</> </>
} }
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}>link</Link> <Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}><T i18nKey="link">#</T></Link>
</li> </li>
{this.props.markable && {this.props.markable &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{`mark as ${node.comment.read ? 'unread' : 'read'}`}</span> <span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{node.comment.read ? i18n.t('mark_as_unread') : i18n.t('mark_as_read')}</span>
</li> </li>
} }
</ul> </ul>
@ -180,23 +182,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div> </div>
{this.state.showRemoveDialog && {this.state.showRemoveDialog &&
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
<button type="submit" class="btn btn-secondary">Remove Comment</button> <button type="submit" class="btn btn-secondary"><T i18nKey="remove_comment">#</T></button>
</form> </form>
} }
{this.state.showBanDialog && {this.state.showBanDialog &&
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label">Reason</label> <label class="col-form-label"><T i18nKey="reason">#</T></label>
<input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
</div> </div>
{/* TODO hold off on expires until later */} {/* TODO hold off on expires until later */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group row">
<button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button> <button type="submit" class="btn btn-secondary">{i18n.t('ban')} {this.props.node.comment.creator_name}</button>
</div> </div>
</form> </form>
} }
@ -387,9 +389,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModBanBothSubmit(i: CommentNode) { handleModBanBothSubmit(i: CommentNode) {
event.preventDefault(); event.preventDefault();
console.log(BanType[i.state.banType]);
console.log(i.props.node.comment.banned);
if (i.state.banType == BanType.Community) { if (i.state.banType == BanType.Community) {
let form: BanFromCommunityForm = { let form: BanFromCommunityForm = {
user_id: i.props.node.comment.creator_id, user_id: i.props.node.comment.creator_id,

View file

@ -32,7 +32,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
moderators={this.props.moderators} moderators={this.props.moderators}
admins={this.props.admins} admins={this.props.admins}
markable={this.props.markable} markable={this.props.markable}
/> />
)} )}
</div> </div>
) )

View file

@ -5,6 +5,8 @@ import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces'; import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
declare const Sortable: any; declare const Sortable: any;
@ -26,12 +28,12 @@ export class Communities extends Component<any, CommunitiesState> {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), (msg) => this.parseMessage(msg),
(err) => console.error(err), (err) => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
this.refetch(); this.refetch();
@ -46,7 +48,7 @@ export class Communities extends Component<any, CommunitiesState> {
} }
componentDidMount() { componentDidMount() {
document.title = `Communities - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('communities')} - ${WebSocketService.Instance.site.name}`;
} }
// Necessary for back button for some reason // Necessary for back button for some reason
@ -64,17 +66,17 @@ export class Communities extends Component<any, CommunitiesState> {
{this.state.loading ? {this.state.loading ?
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
<div> <div>
<h5>List of communities</h5> <h5><T i18nKey="list_of_communities">#</T></h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="community_table" class="table table-sm table-hover"> <table id="community_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">
<tr> <tr>
<th>Name</th> <th><T i18nKey="name">#</T></th>
<th class="d-none d-lg-table-cell">Title</th> <th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th>
<th>Category</th> <th><T i18nKey="category">#</T></th>
<th class="text-right">Subscribers</th> <th class="text-right"><T i18nKey="subscribers">#</T></th>
<th class="text-right d-none d-lg-table-cell">Posts</th> <th class="text-right d-none d-lg-table-cell"><T i18nKey="posts">#</T></th>
<th class="text-right d-none d-lg-table-cell">Comments</th> <th class="text-right d-none d-lg-table-cell"><T i18nKey="comments">#</T></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -89,8 +91,8 @@ export class Communities extends Component<any, CommunitiesState> {
<td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td> <td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
<td class="text-right"> <td class="text-right">
{community.subscribed ? {community.subscribed ?
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</span> : <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></span> :
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</span> <span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></span>
} }
</td> </td>
</tr> </tr>
@ -109,9 +111,9 @@ export class Communities extends Component<any, CommunitiesState> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -165,7 +167,7 @@ export class Communities extends Component<any, CommunitiesState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg; let res: ListCommunitiesResponse = msg;

View file

@ -3,8 +3,10 @@ import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces'; import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp } from '../utils'; import { msgOp, capitalizeFirstLetter } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
import { Community } from '../interfaces'; import { Community } from '../interfaces';
@ -74,25 +76,25 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
return ( return (
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Name</label> <label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
<div class="col-12"> <div class="col-12">
<input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title="lowercase, underscores, and no spaces."/> <input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title={i18n.t('community_reqs')}/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Title</label> <label class="col-12 col-form-label"><T i18nKey="title">#</T></label>
<div class="col-12"> <div class="col-12">
<input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} /> <input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Sidebar</label> <label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
<div class="col-12"> <div class="col-12">
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> <textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Category</label> <label class="col-12 col-form-label"><T i18nKey="category">#</T></label>
<div class="col-12"> <div class="col-12">
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}> <select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}>
{this.state.categories.map(category => {this.state.categories.map(category =>
@ -106,8 +108,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
this.props.community ? 'Save' : 'Create'}</button> this.props.community ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
{this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>} {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
</div> </div>
</div> </div>
</form> </form>
@ -153,7 +155,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
return; return;
@ -169,8 +171,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
this.state.loading = false; this.state.loading = false;
this.props.onCreate(res.community); this.props.onCreate(res.community);
} }
// TODO is ths necessary
// TODO is this necessary?
else if (op == UserOperation.EditCommunity) { else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg; let res: CommunityResponse = msg;
this.state.loading = false; this.state.loading = false;

View file

@ -6,6 +6,7 @@ import { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { Sidebar } from './sidebar'; import { Sidebar } from './sidebar';
import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils'; import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils';
import { T } from 'inferno-i18next';
interface State { interface State {
community: CommunityI; community: CommunityI;
@ -102,7 +103,7 @@ export class Community extends Component<any, State> {
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<h5>{this.state.community.title} <h5>{this.state.community.title}
{this.state.community.removed && {this.state.community.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
} }
</h5> </h5>
{this.selects()} {this.selects()}
@ -126,15 +127,15 @@ export class Community extends Component<any, State> {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto">
<option disabled>Sort Type</option> <option disabled><T i18nKey="sort_type">#</T></option>
<option value={SortType.Hot}>Hot</option> <option value={SortType.Hot}><T i18nKey="hot">#</T></option>
<option value={SortType.New}>New</option> <option value={SortType.New}><T i18nKey="new">#</T></option>
<option disabled></option> <option disabled></option>
<option value={SortType.TopDay}>Top Day</option> <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
<option value={SortType.TopWeek}>Week</option> <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
<option value={SortType.TopMonth}>Month</option> <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
<option value={SortType.TopYear}>Year</option> <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
</select> </select>
</div> </div>
) )
@ -144,9 +145,9 @@ export class Community extends Component<any, State> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -193,7 +194,7 @@ export class Community extends Component<any, State> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let res: GetCommunityResponse = msg; let res: GetCommunityResponse = msg;

View file

@ -2,6 +2,8 @@ import { Component } from 'inferno';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
import { Community } from '../interfaces'; import { Community } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
export class CreateCommunity extends Component<any, any> { export class CreateCommunity extends Component<any, any> {
@ -11,7 +13,7 @@ export class CreateCommunity extends Component<any, any> {
} }
componentDidMount() { componentDidMount() {
document.title = `Create Community - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -19,7 +21,7 @@ export class CreateCommunity extends Component<any, any> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>Create Community</h5> <h5><T i18nKey="create_community">#</T></h5>
<CommunityForm onCreate={this.handleCommunityCreate}/> <CommunityForm onCreate={this.handleCommunityCreate}/>
</div> </div>
</div> </div>

View file

@ -1,6 +1,8 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
export class CreatePost extends Component<any, any> { export class CreatePost extends Component<any, any> {
@ -10,7 +12,7 @@ export class CreatePost extends Component<any, any> {
} }
componentDidMount() { componentDidMount() {
document.title = `Create Post - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -18,7 +20,7 @@ export class CreatePost extends Component<any, any> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>Create a Post</h5> <h5><T i18nKey="create_post">#</T></h5>
<PostForm onCreate={this.handlePostCreate} prevCommunityName={this.prevCommunityName} /> <PostForm onCreate={this.handlePostCreate} prevCommunityName={this.prevCommunityName} />
</div> </div>
</div> </div>

View file

@ -2,6 +2,7 @@ import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { repoUrl } from '../utils'; import { repoUrl } from '../utils';
import { version } from '../version'; import { version } from '../version';
import { T } from 'inferno-i18next';
export class Footer extends Component<any, any> { export class Footer extends Component<any, any> {
@ -19,16 +20,16 @@ export class Footer extends Component<any, any> {
<span class="navbar-text">{version}</span> <span class="navbar-text">{version}</span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/modlog">Modlog</Link> <Link class="nav-link" to="/modlog"><T i18nKey="modlog">#</T></Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>API</a> <a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}><T i18nKey="api">#</T></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/sponsors">Sponsors</Link> <Link class="nav-link" to="/sponsors"><T i18nKey="sponsors">#</T></Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={repoUrl}>Code</a> <a class="nav-link" href={repoUrl}><T i18nKey="code">#</T></a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -6,6 +6,8 @@ import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, C
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
enum UnreadType { enum UnreadType {
Unread, All Unread, All
@ -49,7 +51,7 @@ export class Inbox extends Component<any, InboxState> {
} }
componentDidMount() { componentDidMount() {
document.title = `/u/${UserService.Instance.user.username} Inbox - ${WebSocketService.Instance.site.name}`; document.title = `/u/${UserService.Instance.user.username} ${i18n.t('inbox')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -59,12 +61,12 @@ export class Inbox extends Component<any, InboxState> {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5 class="mb-0"> <h5 class="mb-0">
<span>Inbox for <Link to={`/u/${user.username}`}>{user.username}</Link></span> <span><T i18nKey="inbox_for" interpolation={{user: user.username}}>#<Link to={`/u/${user.username}`}>#</Link></T></span>
</h5> </h5>
{this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread && {this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={this.markAllAsRead}>mark all as read</span> <span class="pointer" onClick={this.markAllAsRead}><T i18nKey="mark_all_as_read">#</T></span>
</li> </li>
</ul> </ul>
} }
@ -81,18 +83,18 @@ export class Inbox extends Component<any, InboxState> {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto"> <select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto">
<option disabled>Type</option> <option disabled><T i18nKey="type">#</T></option>
<option value={UnreadType.Unread}>Unread</option> <option value={UnreadType.Unread}><T i18nKey="unread">#</T></option>
<option value={UnreadType.All}>All</option> <option value={UnreadType.All}><T i18nKey="all">#</T></option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
<option disabled>Sort Type</option> <option disabled><T i18nKey="sort_type">#</T></option>
<option value={SortType.New}>New</option> <option value={SortType.New}><T i18nKey="new">#</T></option>
<option value={SortType.TopDay}>Top Day</option> <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
<option value={SortType.TopWeek}>Week</option> <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
<option value={SortType.TopMonth}>Month</option> <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
<option value={SortType.TopYear}>Year</option> <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
</select> </select>
</div> </div>
) )
@ -113,9 +115,9 @@ export class Inbox extends Component<any, InboxState> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -164,7 +166,7 @@ export class Inbox extends Component<any, InboxState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) { } else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) {
let res: GetRepliesResponse = msg; let res: GetRepliesResponse = msg;
@ -196,7 +198,7 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
// let res: CommentResponse = msg; // let res: CommentResponse = msg;
alert('Reply sent'); alert(i18n.t('reply_sent'));
// this.state.replies.unshift(res.comment); // TODO do this right // this.state.replies.unshift(res.comment); // TODO do this right
// this.setState(this.state); // this.setState(this.state);
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {

View file

@ -4,6 +4,8 @@ import { retryWhen, delay, take } from 'rxjs/operators';
import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces'; import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface State { interface State {
loginForm: LoginForm; loginForm: LoginForm;
@ -50,7 +52,7 @@ export class Login extends Component<any, State> {
} }
componentDidMount() { componentDidMount() {
document.title = `Login - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -74,13 +76,13 @@ export class Login extends Component<any, State> {
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}> <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<h5>Login</h5> <h5>Login</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Email or Username</label> <label class="col-sm-2 col-form-label"><T i18nKey="email_or_username">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> <input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Password</label> <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required /> <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required />
</div> </div>
@ -88,38 +90,37 @@ export class Login extends Component<any, State> {
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.loginLoading ? <button type="submit" class="btn btn-secondary">{this.state.loginLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Login'}</button> <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('login')}</button>
</div> </div>
</div> </div>
</form> </form>
{/* Forgot your password or deleted your account? Reset your password. TODO */}
</div> </div>
); );
} }
registerForm() { registerForm() {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>Sign Up</h5> <h5><T i18nKey="sign_up">#</T></h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label> <label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" /> <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Email</label> <label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="email" class="form-control" placeholder="Optional" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> <input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Password</label> <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Verify Password</label> <label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
</div> </div>
@ -127,7 +128,7 @@ export class Login extends Component<any, State> {
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ? <button type="submit" class="btn btn-secondary">{this.state.registerLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button> <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
</div> </div>
</div> </div>
@ -183,7 +184,7 @@ export class Login extends Component<any, State> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
this.state = this.emptyState; this.state = this.emptyState;
this.setState(this.state); this.setState(this.state);
return; return;

View file

@ -7,6 +7,8 @@ import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { SiteForm } from './site-form'; import { SiteForm } from './site-form';
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils'; import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface MainState { interface MainState {
subscribedCommunities: Array<CommunityUser>; subscribedCommunities: Array<CommunityUser>;
@ -120,34 +122,48 @@ export class Main extends Component<any, MainState> {
{this.posts()} {this.posts()}
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{!this.state.loading && {this.my_sidebar()}
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h5>Subscribed <Link class="text-white" to="/communities">communities</Link></h5>
<ul class="list-inline">
{this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
)}
</ul>
</div>
}
<Link class="btn btn-sm btn-secondary btn-block mb-3"
to="/create_community">Create a Community</Link>
{this.sidebar()}
</div>
}
</div> </div>
</div> </div>
</div> </div>
) )
} }
my_sidebar() {
return(
<div>
{!this.state.loading &&
<div>
{this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 &&
<div>
<h5>
<T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T>
</h5>
<ul class="list-inline">
{this.state.subscribedCommunities.map(community =>
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
)}
</ul>
</div>
}
<Link class="btn btn-sm btn-secondary btn-block mb-3"
to="/create_community">
<T i18nKey="create_a_community">#</T>
</Link>
{this.sidebar()}
</div>
}
</div>
)
}
trendingCommunities() { trendingCommunities() {
return ( return (
<div> <div>
<h5>Trending <Link class="text-white" to="/communities">communities</Link></h5> <h5>
<T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T>
</h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.trendingCommunities.map(community => {this.state.trendingCommunities.map(community =>
<li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li> <li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li>
@ -185,18 +201,32 @@ export class Main extends Component<any, MainState> {
{this.canAdmin && {this.canAdmin &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>
<T i18nKey="edit">#</T>
</span>
</li> </li>
</ul> </ul>
} }
<ul class="my-2 list-inline"> <ul class="my-2 list-inline">
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_users} Users</li> <li className="list-inline-item badge badge-light">
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_posts} Posts</li> <T i18nKey="number_of_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T>
<li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</li> </li>
<li className="list-inline-item"><Link className="badge badge-light" to="/modlog">Modlog</Link></li> <li className="list-inline-item badge badge-light">
<T i18nKey="number_of_posts" interpolation={{count: this.state.site.site.number_of_posts}}>#</T>
</li>
<li className="list-inline-item badge badge-light">
<T i18nKey="number_of_comments" interpolation={{count: this.state.site.site.number_of_comments}}>#</T>
</li>
<li className="list-inline-item">
<Link className="badge badge-light" to="/modlog">
<T i18nKey="modlog">#</T>
</Link>
</li>
</ul> </ul>
<ul class="my-1 list-inline small"> <ul class="my-1 list-inline small">
<li class="list-inline-item">admins: </li> <li class="list-inline-item">
<T i18nKey="admins" class="d-inline">#</T>:
</li>
{this.state.site.admins.map(admin => {this.state.site.admins.map(admin =>
<li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li> <li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li>
)} )}
@ -215,15 +245,15 @@ export class Main extends Component<any, MainState> {
landing() { landing() {
return ( return (
<div> <div>
<h5>Powered by <h5>
<svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg> <T i18nKey="powered_by" class="d-inline">#</T>
<a href={repoUrl}>Lemmy<sup>Beta</sup></a> <svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg>
<a href={repoUrl}>Lemmy<sup>beta</sup></a>
</h5> </h5>
<p>Lemmy is a <a href="https://en.wikipedia.org/wiki/Link_aggregation">link aggregator</a> / reddit alternative, intended to work in the <a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p> <p>
<p>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p> <T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/Link_aggregation">#</a><a href="https://en.wikipedia.org/wiki/Fediverse">#</a><br></br><code>#</code><br></br><b>#</b><br></br><a href={repoUrl}>#</a><br></br><a href="https://www.rust-lang.org">#</a><a href="https://actix.rs/">#</a><a href="https://www.infernojs.org">#</a><a href="https://www.typescriptlang.org/">#</a>
<p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p> </T>
<p>Suggest new features or report bugs <a href={repoUrl}>here.</a></p> </p>
<p>Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, <a href="https://www.typescriptlang.org/">Typescript</a>.</p>
</div> </div>
) )
} }
@ -257,7 +287,7 @@ export class Main extends Component<any, MainState> {
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
disabled={UserService.Instance.user == undefined} disabled={UserService.Instance.user == undefined}
/> />
Subscribed {i18n.t('subscribed')}
</label> </label>
<label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}> <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}>
<input type="radio" <input type="radio"
@ -265,19 +295,19 @@ export class Main extends Component<any, MainState> {
checked={this.state.type_ == ListingType.All} checked={this.state.type_ == ListingType.All}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
/> />
All {i18n.t('all')}
</label> </label>
</div> </div>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto">
<option disabled>Sort Type</option> <option disabled><T i18nKey="sort_type">#</T></option>
<option value={SortType.Hot}>Hot</option> <option value={SortType.Hot}><T i18nKey="hot">#</T></option>
<option value={SortType.New}>New</option> <option value={SortType.New}><T i18nKey="new">#</T></option>
<option disabled></option> <option disabled></option>
<option value={SortType.TopDay}>Top Day</option> <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
<option value={SortType.TopWeek}>Week</option> <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
<option value={SortType.TopMonth}>Month</option> <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
<option value={SortType.TopYear}>Year</option> <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
</select> </select>
</div> </div>
) )
@ -287,9 +317,9 @@ export class Main extends Component<any, MainState> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -352,7 +382,7 @@ export class Main extends Component<any, MainState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetFollowedCommunities) { } else if (op == UserOperation.GetFollowedCommunities) {
let res: GetFollowedCommunitiesResponse = msg; let res: GetFollowedCommunitiesResponse = msg;

View file

@ -223,7 +223,7 @@ export class Modlog extends Component<any, ModlogState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetModlog) { } else if (op == UserOperation.GetModlog) {
let res: GetModlogResponse = msg; let res: GetModlogResponse = msg;

View file

@ -1,5 +1,8 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import * as moment from 'moment'; import * as moment from 'moment';
// import 'moment/locale/de.js';
import { getLanguage } from '../utils';
import { i18n } from '../i18next';
interface MomentTimeProps { interface MomentTimeProps {
data: { data: {
@ -13,12 +16,13 @@ export class MomentTime extends Component<MomentTimeProps, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
moment.locale(getLanguage());
} }
render() { render() {
if (this.props.data.updated) { if (this.props.data.updated) {
return ( return (
<span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span> <span title={this.props.data.updated} className="font-italics">{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}</span>
) )
} else { } else {
let str = this.props.data.published || this.props.data.when_; let str = this.props.data.published || this.props.data.when_;

View file

@ -6,6 +6,8 @@ import { WebSocketService, UserService } from '../services';
import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces'; import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { version } from '../version'; import { version } from '../version';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface NavbarState { interface NavbarState {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -85,16 +87,16 @@ export class Navbar extends Component<any, NavbarState> {
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}> <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/communities">Communities</Link> <Link class="nav-link" to="/communities"><T i18nKey="communities">#</T></Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/search">Search</Link> <Link class="nav-link" to="/search"><T i18nKey="search">#</T></Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}>Create Post</Link> <Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}><T i18nKey="create_post">#</T></Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/create_community">Create Community</Link> <Link class="nav-link" to="/create_community"><T i18nKey="create_community">#</T></Link>
</li> </li>
</ul> </ul>
<ul class="navbar-nav ml-auto mr-2"> <ul class="navbar-nav ml-auto mr-2">
@ -113,13 +115,13 @@ export class Navbar extends Component<any, NavbarState> {
{UserService.Instance.user.username} {UserService.Instance.user.username}
</a> </a>
<div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}> <div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}>
<a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}>Overview</a> <a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}><T i18nKey="overview">#</T></a>
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a> <a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }><T i18nKey="logout">#</T></a>
</div> </div>
</li> </li>
</> </>
: :
<Link class="nav-link" to="/login">Login / Sign up</Link> <Link class="nav-link" to="/login"><T i18nKey="login_sign_up">#</T></Link>
} }
</ul> </ul>
</div> </div>
@ -153,6 +155,7 @@ export class Navbar extends Component<any, NavbarState> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
// TODO
if (msg.error == "Not logged in.") { if (msg.error == "Not logged in.") {
UserService.Instance.logout(); UserService.Instance.logout();
location.reload(); location.reload();
@ -209,7 +212,7 @@ export class Navbar extends Component<any, NavbarState> {
if (UserService.Instance.user) { if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
if (!Notification) { if (!Notification) {
alert('Desktop notifications not available in your browser. Try Chromium.'); alert(i18n.t('notifications_error'));
return; return;
} }
@ -224,7 +227,7 @@ export class Navbar extends Component<any, NavbarState> {
if (Notification.permission !== 'granted') if (Notification.permission !== 'granted')
Notification.requestPermission(); Notification.requestPermission();
else { else {
var notification = new Notification(`${replies.length} Unread Messages`, { var notification = new Notification(`${replies.length} ${i18n.t('unread_messages')}`, {
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`, icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
body: `${recentReply.creator_name}: ${recentReply.content}` body: `${recentReply.creator_name}: ${recentReply.content}`
}); });

View file

@ -4,8 +4,10 @@ import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces'; import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, getPageTitle, debounce } from '../utils'; import { msgOp, getPageTitle, debounce, capitalizeFirstLetter } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface PostFormProps { interface PostFormProps {
post?: Post; // If a post is given, that means this is an edit post?: Post; // If a post is given, that means this is an edit
@ -85,28 +87,28 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div> <div>
<form onSubmit={linkEvent(this, this.handlePostSubmit)}> <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">URL</label> <label class="col-sm-2 col-form-label"><T i18nKey="url">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, debounce(this.handlePostUrlChange))} /> <input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, debounce(this.handlePostUrlChange))} />
{this.state.suggestedTitle && {this.state.suggestedTitle &&
<div class="mt-1 text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}>copy suggested title: {this.state.suggestedTitle}</div> <div class="mt-1 text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}><T i18nKey="copy_suggested_title" interpolation={{title: this.state.suggestedTitle}}>#</T></div>
} }
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Title</label> <label class="col-sm-2 col-form-label"><T i18nKey="title">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea value={this.state.postForm.name} onInput={linkEvent(this, debounce(this.handlePostNameChange))} class="form-control" required rows={2} minLength={3} maxLength={100} /> <textarea value={this.state.postForm.name} onInput={linkEvent(this, debounce(this.handlePostNameChange))} class="form-control" required rows={2} minLength={3} maxLength={100} />
{this.state.suggestedPosts.length > 0 && {this.state.suggestedPosts.length > 0 &&
<> <>
<div class="my-1 text-muted small font-weight-bold">These posts might be related</div> <div class="my-1 text-muted small font-weight-bold"><T i18nKey="related_posts">#</T></div>
<PostListings posts={this.state.suggestedPosts} /> <PostListings posts={this.state.suggestedPosts} />
</> </>
} }
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Body</label> <label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} /> <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} />
</div> </div>
@ -114,7 +116,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{/* Cant change a community from an edit */} {/* Cant change a community from an edit */}
{!this.props.post && {!this.props.post &&
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Community</label> <label class="col-sm-2 col-form-label"><T i18nKey="community">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}> <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
{this.state.communities.map(community => {this.state.communities.map(community =>
@ -129,8 +131,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
this.props.post ? 'Save' : 'Create'}</button> this.props.post ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('Create'))}</button>
{this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>} {this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
</div> </div>
</div> </div>
</form> </form>
@ -201,7 +203,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
return; return;

View file

@ -5,6 +5,8 @@ import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, Communit
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { mdToHtml, canMod, isMod, isImage } from '../utils'; import { mdToHtml, canMod, isMod, isImage } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface PostListingState { interface PostListingState {
showEdit: boolean; showEdit: boolean;
@ -67,14 +69,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</div> </div>
</div> </div>
{post.url && isImage(post.url) && {post.url && isImage(post.url) &&
<span title="Expand here" class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 float-left img-fluid thumbnail rounded" src={post.url} /></span> <span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 float-left img-fluid thumbnail rounded" src={post.url} /></span>
} }
<div className="ml-4"> <div className="ml-4">
<div> <div>
<h5 className="mb-0 d-inline"> <h5 className="mb-0 d-inline">
{post.url ? {post.url ?
<a className="text-white" href={post.url} target="_blank" title={post.url}>{post.name}</a> : <a className="text-white" href={post.url} target="_blank" title={post.url}>{post.name}</a> :
<Link className="text-white" to={`/post/${post.id}`} title="Comments">{post.name}</Link> <Link className="text-white" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
} }
</h5> </h5>
{post.url && {post.url &&
@ -83,18 +85,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</small> </small>
} }
{post.removed && {post.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
} }
{post.deleted && {post.deleted &&
<small className="ml-2 text-muted font-italic">deleted</small> <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
} }
{post.locked && {post.locked &&
<small className="ml-2 text-muted font-italic">locked</small> <small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
} }
{ post.url && isImage(post.url) && { post.url && isImage(post.url) &&
<> <>
{ !this.state.imageExpanded { !this.state.imageExpanded
? <span class="text-monospace pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span> ? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
: :
<span> <span>
<span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span> <span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span>
@ -113,10 +115,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<span>by </span> <span>by </span>
<Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link> <Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
{this.isMod && {this.isMod &&
<span className="mx-1 badge badge-light">mod</span> <span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
} }
{this.isAdmin && {this.isAdmin &&
<span className="mx-1 badge badge-light">admin</span> <span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span>
} }
{this.props.showCommunity && {this.props.showCommunity &&
<span> <span>
@ -137,22 +139,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</span> </span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link> <Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link>
</li> </li>
</ul> </ul>
{UserService.Instance.user && this.props.editable && {UserService.Instance.user && this.props.editable &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? 'unsave' : 'save'}</span> <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span>
</li> </li>
{this.myPost && {this.myPost &&
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
{!post.deleted ? 'delete' : 'restore'} {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
</span> </span>
</li> </li>
</> </>
@ -161,12 +163,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<span> <span>
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.post.removed ? {!this.props.post.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
} }
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? 'unlock' : 'lock'}</span> <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? i18n.t('unlock') : i18n.t('lock')}</span>
</li> </li>
</span> </span>
} }
@ -174,8 +176,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
{this.state.showRemoveDialog && {this.state.showRemoveDialog &&
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
<button type="submit" class="btn btn-secondary">Remove Post</button> <button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button>
</form> </form>
} }
{this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />} {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />}

View file

@ -2,6 +2,7 @@ import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Post } from '../interfaces'; import { Post } from '../interfaces';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { T } from 'inferno-i18next';
interface PostListingsProps { interface PostListingsProps {
posts: Array<Post>; posts: Array<Post>;
@ -19,8 +20,10 @@ export class PostListings extends Component<PostListingsProps, any> {
<div> <div>
{this.props.posts.length > 0 ? this.props.posts.map(post => {this.props.posts.length > 0 ? this.props.posts.map(post =>
<PostListing post={post} showCommunity={this.props.showCommunity} />) : <PostListing post={post} showCommunity={this.props.showCommunity} />) :
<div>No posts. {this.props.showCommunity !== undefined && <span>Subscribe to some <Link to="/communities">communities</Link>.</span>} <>
</div> <div><T i18nKey="no_posts">#</T></div>
{this.props.showCommunity !== undefined && <div><T i18nKey="subscribe_to_communities">#<Link to="/communities">#</Link></T></div>}
</>
} }
</div> </div>
) )

View file

@ -9,6 +9,8 @@ import { Sidebar } from './sidebar';
import { CommentForm } from './comment-form'; import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface PostState { interface PostState {
post: PostI; post: PostI;
@ -130,17 +132,17 @@ export class Post extends Component<any, PostState> {
sortRadios() { sortRadios() {
return ( return (
<div class="btn-group btn-group-toggle mb-3"> <div class="btn-group btn-group-toggle mb-3">
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>{i18n.t('hot')}
<input type="radio" value={CommentSortType.Hot} <input type="radio" value={CommentSortType.Hot}
checked={this.state.commentSort === CommentSortType.Hot} checked={this.state.commentSort === CommentSortType.Hot}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
</label> </label>
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>{i18n.t('top')}
<input type="radio" value={CommentSortType.Top} <input type="radio" value={CommentSortType.Top}
checked={this.state.commentSort === CommentSortType.Top} checked={this.state.commentSort === CommentSortType.Top}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
</label> </label>
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>New <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>{i18n.t('new')}
<input type="radio" value={CommentSortType.New} <input type="radio" value={CommentSortType.New}
checked={this.state.commentSort === CommentSortType.New} checked={this.state.commentSort === CommentSortType.New}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)} />
@ -152,7 +154,7 @@ export class Post extends Component<any, PostState> {
newComments() { newComments() {
return ( return (
<div class="container-fluid sticky-top new-comments"> <div class="container-fluid sticky-top new-comments">
<h5>Chat</h5> <h5><T i18nKey="chat">#</T></h5>
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} /> <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
{this.state.comments.map(comment => {this.state.comments.map(comment =>
<CommentNodes <CommentNodes
@ -242,7 +244,7 @@ export class Post extends Component<any, PostState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let res: GetPostResponse = msg; let res: GetPostResponse = msg;

View file

@ -6,6 +6,8 @@ import { WebSocketService } from '../services';
import { msgOp, fetchLimit } from '../utils'; import { msgOp, fetchLimit } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface SearchState { interface SearchState {
q: string, q: string,
@ -52,7 +54,7 @@ export class Search extends Component<any, SearchState> {
} }
componentDidMount() { componentDidMount() {
document.title = `Search - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -60,7 +62,7 @@ export class Search extends Component<any, SearchState> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5>Search</h5> <h5><T i18nKey="search">#</T></h5>
{this.selects()} {this.selects()}
{this.searchForm()} {this.searchForm()}
{this.state.type_ == SearchType.Both && {this.state.type_ == SearchType.Both &&
@ -83,11 +85,11 @@ export class Search extends Component<any, SearchState> {
searchForm() { searchForm() {
return ( return (
<form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}> <form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
<input type="text" class="form-control mr-2" value={this.state.q} placeholder="Search..." onInput={linkEvent(this, this.handleQChange)} required minLength={3} /> <input type="text" class="form-control mr-2" value={this.state.q} placeholder={`${i18n.t('search')}...`} onInput={linkEvent(this, this.handleQChange)} required minLength={3} />
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
<span>Search</span> <span><T i18nKey="search">#</T></span>
} }
</button> </button>
</form> </form>
@ -98,19 +100,19 @@ export class Search extends Component<any, SearchState> {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto"> <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
<option disabled>Type</option> <option disabled><T i18nKey="type">#</T></option>
<option value={SearchType.Both}>Both</option> <option value={SearchType.Both}><T i18nKey="both">#</T></option>
<option value={SearchType.Comments}>Comments</option> <option value={SearchType.Comments}><T i18nKey="comments">#</T></option>
<option value={SearchType.Posts}>Posts</option> <option value={SearchType.Posts}><T i18nKey="posts">#</T></option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
<option disabled>Sort Type</option> <option disabled><T i18nKey="sort_type">#</T></option>
<option value={SortType.New}>New</option> <option value={SortType.New}><T i18nKey="new">#</T></option>
<option value={SortType.TopDay}>Top Day</option> <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
<option value={SortType.TopWeek}>Week</option> <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
<option value={SortType.TopMonth}>Month</option> <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
<option value={SortType.TopYear}>Year</option> <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
</select> </select>
</div> </div>
) )
@ -171,9 +173,9 @@ export class Search extends Component<any, SearchState> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -183,7 +185,7 @@ export class Search extends Component<any, SearchState> {
return ( return (
<div> <div>
{res && res.op && res.posts.length == 0 && res.comments.length == 0 && {res && res.op && res.posts.length == 0 && res.comments.length == 0 &&
<span>No Results</span> <span><T i18nKey="no_results">#</T></span>
} }
</div> </div>
) )
@ -244,13 +246,13 @@ export class Search extends Component<any, SearchState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let res: SearchResponse = msg; let res: SearchResponse = msg;
this.state.searchResponse = res; this.state.searchResponse = res;
this.state.loading = false; this.state.loading = false;
document.title = `Search - ${this.state.q} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0,0); window.scrollTo(0,0);
this.setState(this.state); this.setState(this.state);
} }

View file

@ -5,6 +5,8 @@ import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { SiteForm } from './site-form'; import { SiteForm } from './site-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface State { interface State {
userForm: RegisterForm; userForm: RegisterForm;
@ -46,7 +48,7 @@ export class Setup extends Component<any, State> {
} }
componentDidMount() { componentDidMount() {
document.title = "Setup - Lemmy"; document.title = `${i18n.t('setup')} - Lemmy`;
} }
render() { render() {
@ -54,7 +56,7 @@ export class Setup extends Component<any, State> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 offset-lg-3 col-lg-6"> <div class="col-12 offset-lg-3 col-lg-6">
<h3>Lemmy Instance Setup</h3> <h3><T i18nKey="lemmy_instance_setup">#</T></h3>
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />} {!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />}
</div> </div>
</div> </div>
@ -65,27 +67,27 @@ export class Setup extends Component<any, State> {
registerUser() { registerUser() {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>Set up Site Administrator</h5> <h5><T i18nKey="setup_admin">#</T></h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Username</label> <label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" /> <input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Email</label> <label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="email" class="form-control" placeholder="Optional" value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> <input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Password</label> <label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> <input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Verify Password</label> <label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> <input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
</div> </div>
@ -93,7 +95,7 @@ export class Setup extends Component<any, State> {
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.userLoading ? <button type="submit" class="btn btn-secondary">{this.state.userLoading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : 'Sign Up'}</button> <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button>
</div> </div>
</div> </div>
@ -133,7 +135,7 @@ export class Setup extends Component<any, State> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
this.state.userLoading = false; this.state.userLoading = false;
this.setState(this.state); this.setState(this.state);
return; return;

View file

@ -4,6 +4,8 @@ import { Community, CommunityUser, FollowCommunityForm, CommunityForm as Communi
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils'; import { mdToHtml, getUnixTime } from '../utils';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface SidebarProps { interface SidebarProps {
community: Community; community: Community;
@ -54,10 +56,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div> <div>
<h5 className="mb-0">{community.title} <h5 className="mb-0">{community.title}
{community.removed && {community.removed &&
<small className="ml-2 text-muted font-italic">removed</small> <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
} }
{community.deleted && {community.deleted &&
<small className="ml-2 text-muted font-italic">deleted</small> <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
} }
</h5> </h5>
<Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link> <Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link>
@ -65,12 +67,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.canMod && {this.canMod &&
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
</li> </li>
{this.amCreator && {this.amCreator &&
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
{!community.deleted ? 'delete' : 'restore'} {!community.deleted ? i18n.t('delete') : i18n.t('restore')}
</span> </span>
</li> </li>
} }
@ -79,8 +81,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.canAdmin && {this.canAdmin &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.community.removed ? {!this.props.community.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
} }
</li> </li>
@ -89,38 +91,38 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.state.showRemoveDialog && {this.state.showRemoveDialog &&
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label">Reason</label> <label class="col-form-label"><T i18nKey="reason">#</T></label>
<input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> <input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
</div> </div>
{/* TODO hold off on expires for now */} {/* TODO hold off on expires for now */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group row">
<button type="submit" class="btn btn-secondary">Remove Community</button> <button type="submit" class="btn btn-secondary"><T i18nKey="remove_community">#</T></button>
</div> </div>
</form> </form>
} }
<ul class="my-1 list-inline"> <ul class="my-1 list-inline">
<li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li> <li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
<li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li> <li className="list-inline-item badge badge-light"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></li>
<li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li> <li className="list-inline-item badge badge-light"><T i18nKey="number_of_posts" interpolation={{count: community.number_of_posts}}>#</T></li>
<li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li> <li className="list-inline-item badge badge-light"><T i18nKey="number_of_comments" interpolation={{count: community.number_of_comments}}>#</T></li>
<li className="list-inline-item"><Link className="badge badge-light" to={`/modlog/community/${this.props.community.id}`}>Modlog</Link></li> <li className="list-inline-item"><Link className="badge badge-light" to={`/modlog/community/${this.props.community.id}`}><T i18nKey="modlog">#</T></Link></li>
</ul> </ul>
<ul class="list-inline small"> <ul class="list-inline small">
<li class="list-inline-item">mods: </li> <li class="list-inline-item">{i18n.t('mods')}: </li>
{this.props.moderators.map(mod => {this.props.moderators.map(mod =>
<li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li> <li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li>
)} )}
</ul> </ul>
<Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`} <Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`}
to={`/create_post/c/${community.name}`}>Create a Post</Link> to={`/create_post/c/${community.name}`}><T i18nKey="create_a_post">#</T></Link>
<div> <div>
{community.subscribed {community.subscribed
? <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button> ? <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></button>
: <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button> : <button class="btn btn-sm btn-secondary btn-block mb-3" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></button>
} }
</div> </div>
{community.description && {community.description &&

View file

@ -1,7 +1,10 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Site, SiteForm as SiteFormI } from '../interfaces'; import { Site, SiteForm as SiteFormI } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { capitalizeFirstLetter } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface SiteFormProps { interface SiteFormProps {
site?: Site; // If a site is given, that means this is an edit site?: Site; // If a site is given, that means this is an edit
@ -39,15 +42,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
render() { render() {
return ( return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}> <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h5>{`${this.props.site ? 'Edit' : 'Name'} your Site`}</h5> <h5>{`${this.props.site ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('name'))} ${i18n.t('your_site')}`}</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Name</label> <label class="col-12 col-form-label"><T i18nKey="name">#</T></label>
<div class="col-12"> <div class="col-12">
<input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} /> <input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label">Sidebar</label> <label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label>
<div class="col-12"> <div class="col-12">
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> <textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} />
</div> </div>
@ -57,8 +60,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ?
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
this.props.site ? 'Save' : 'Create'}</button> this.props.site ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button>
{this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</button>} {this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>}
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,5 +1,7 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
let general = let general =
[ [
@ -18,7 +20,7 @@ export class Sponsors extends Component<any, any> {
} }
componentDidMount() { componentDidMount() {
document.title = `Sponsors - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -36,19 +38,19 @@ export class Sponsors extends Component<any, any> {
topMessage() { topMessage() {
return ( return (
<div> <div>
<h5>Sponsors of Lemmy</h5> <h5><T i18nKey="sponsors_of_lemmy">#</T></h5>
<p> <p>
Lemmy is free, <a href="https://github.com/dessalines/lemmy">open-source</a> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people: <T i18nKey="sponsor_message">#<a href="https://github.com/dessalines/lemmy">#</a></T>
</p> </p>
<a class="btn btn-secondary" href="https://www.patreon.com/dessalines">Support on Patreon</a> <a class="btn btn-secondary" href="https://www.patreon.com/dessalines"><T i18nKey="support_on_patreon">#</T></a>
</div> </div>
) )
} }
sponsors() { sponsors() {
return ( return (
<div class="container"> <div class="container">
<h5>Sponsors</h5> <h5><T i18nKey="sponsors">#</T></h5>
<p>General Sponsors are those that pledged $10 to $39 to Lemmy.</p> <p><T i18nKey="general_sponsors">#</T></p>
<div class="row card-columns"> <div class="row card-columns">
{general.map(s => {general.map(s =>
<div class="card col-12 col-md-2"> <div class="card col-12 col-md-2">
@ -63,16 +65,16 @@ export class Sponsors extends Component<any, any> {
bitcoin() { bitcoin() {
return ( return (
<div> <div>
<h5>Crypto</h5> <h5><T i18nKey="crypto">#</T></h5>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover text-center"> <table class="table table-hover text-center">
<tbody> <tbody>
<tr> <tr>
<td>Bitcoin</td> <td><T i18nKey="bitcoin">#</T></td>
<td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td> <td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td>
</tr> </tr>
<tr> <tr>
<td>Ethereum</td> <td><T i18nKey="ethereum">#</T></td>
<td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td> <td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td>
</tr> </tr>
</tbody> </tbody>

View file

@ -8,6 +8,8 @@ import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '.
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
enum View { enum View {
Overview, Comments, Posts, Saved Overview, Comments, Posts, Saved
@ -142,20 +144,20 @@ export class User extends Component<any, UserState> {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto"> <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
<option disabled>View</option> <option disabled><T i18nKey="view">#</T></option>
<option value={View.Overview}>Overview</option> <option value={View.Overview}><T i18nKey="overview">#</T></option>
<option value={View.Comments}>Comments</option> <option value={View.Comments}><T i18nKey="comments">#</T></option>
<option value={View.Posts}>Posts</option> <option value={View.Posts}><T i18nKey="posts">#</T></option>
<option value={View.Saved}>Saved</option> <option value={View.Saved}><T i18nKey="saved">#</T></option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
<option disabled>Sort Type</option> <option disabled><T i18nKey="sort_type">#</T></option>
<option value={SortType.New}>New</option> <option value={SortType.New}><T i18nKey="new">#</T></option>
<option value={SortType.TopDay}>Top Day</option> <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
<option value={SortType.TopWeek}>Week</option> <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
<option value={SortType.TopMonth}>Month</option> <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
<option value={SortType.TopYear}>Year</option> <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
<option value={SortType.TopAll}>All</option> <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
</select> </select>
</div> </div>
) )
@ -217,15 +219,15 @@ export class User extends Component<any, UserState> {
return ( return (
<div> <div>
<h5>{user.name}</h5> <h5>{user.name}</h5>
<div>Joined <MomentTime data={user} /></div> <div>{i18n.t('joined')}<MomentTime data={user} /></div>
<table class="table table-bordered table-sm mt-2"> <table class="table table-bordered table-sm mt-2">
<tr> <tr>
<td>{user.post_score} points</td> <td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
<td>{user.number_of_posts} posts</td> <td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td>
</tr> </tr>
<tr> <tr>
<td>{user.comment_score} points</td> <td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td>
<td>{user.number_of_comments} comments</td> <td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td>
</tr> </tr>
</table> </table>
<hr /> <hr />
@ -238,7 +240,7 @@ export class User extends Component<any, UserState> {
<div> <div>
{this.state.moderates.length > 0 && {this.state.moderates.length > 0 &&
<div> <div>
<h5>Moderates</h5> <h5><T i18nKey="moderates">#</T></h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
{this.state.moderates.map(community => {this.state.moderates.map(community =>
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
@ -256,7 +258,7 @@ export class User extends Component<any, UserState> {
{this.state.follows.length > 0 && {this.state.follows.length > 0 &&
<div> <div>
<hr /> <hr />
<h5>Subscribed</h5> <h5><T i18nKey="subscribed">#</T></h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
{this.state.follows.map(community => {this.state.follows.map(community =>
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
@ -272,9 +274,9 @@ export class User extends Component<any, UserState> {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 &&
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
} }
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
</div> </div>
); );
} }
@ -331,7 +333,7 @@ export class User extends Component<any, UserState> {
console.log(msg); console.log(msg);
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
alert(msg.error); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetUserDetails) { } else if (op == UserOperation.GetUserDetails) {
let res: UserDetailsResponse = msg; let res: UserDetailsResponse = msg;
@ -359,7 +361,7 @@ export class User extends Component<any, UserState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
// let res: CommentResponse = msg; // let res: CommentResponse = msg;
alert('Reply sent'); alert(i18n.t('reply_sent'));
// this.state.comments.unshift(res.comment); // TODO do this right // this.state.comments.unshift(res.comment); // TODO do this right
// this.setState(this.state); // this.setState(this.state);
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {

33
ui/src/i18next.ts Normal file
View file

@ -0,0 +1,33 @@
import * as i18n from 'i18next';
import { getLanguage } from './utils';
import { en } from './translations/en';
import { de } from './translations/de';
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
// TODO don't forget to add moment locales for new languages.
const resources = {
en: en,
de: de,
}
function format(value: any, format: any, lng: any) {
if (format === 'uppercase') return value.toUpperCase();
return value;
}
i18n
.init({
debug: true,
// load: 'languageOnly',
// initImmediate: false,
lng: getLanguage(),
fallbackLng: 'en',
resources,
interpolation: {
format: format
}
});
export { i18n, resources };

View file

@ -1,5 +1,6 @@
import { render, Component } from 'inferno'; import { render, Component } from 'inferno';
import { HashRouter, BrowserRouter, Route, Switch } from 'inferno-router'; import { BrowserRouter, Route, Switch } from 'inferno-router';
import { Provider } from 'inferno-i18next';
import { Main } from './components/main'; import { Main } from './components/main';
import { Navbar } from './components/navbar'; import { Navbar } from './components/navbar';
import { Footer } from './components/footer'; import { Footer } from './components/footer';
@ -16,6 +17,7 @@ import { Inbox } from './components/inbox';
import { Search } from './components/search'; import { Search } from './components/search';
import { Sponsors } from './components/sponsors'; import { Sponsors } from './components/sponsors';
import { Symbols } from './components/symbols'; import { Symbols } from './components/symbols';
import { i18n } from './i18next';
import './css/bootstrap.min.css'; import './css/bootstrap.min.css';
import './css/main.css'; import './css/main.css';
@ -34,37 +36,39 @@ class Index extends Component<any, any> {
render() { render() {
return ( return (
<BrowserRouter> <Provider i18next={i18n}>
<Navbar /> <BrowserRouter>
<div class="mt-1 p-0"> <Navbar />
<Switch> <div class="mt-1 p-0">
<Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} /> <Switch>
<Route exact path={`/`} component={Main} /> <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} />
<Route path={`/login`} component={Login} /> <Route exact path={`/`} component={Main} />
<Route path={`/create_post/c/:name`} component={CreatePost} /> <Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} /> <Route path={`/create_post/c/:name`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} /> <Route path={`/create_post`} component={CreatePost} />
<Route path={`/communities/page/:page`} component={Communities} /> <Route path={`/create_community`} component={CreateCommunity} />
<Route path={`/communities`} component={Communities} /> <Route path={`/communities/page/:page`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} /> <Route path={`/communities`} component={Communities} />
<Route path={`/post/:id`} component={Post} /> <Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} /> <Route path={`/post/:id`} component={Post} />
<Route path={`/community/:id`} component={Community} /> <Route path={`/c/:name/sort/:sort/page/:page`} component={Community} />
<Route path={`/c/:name`} component={Community} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} /> <Route path={`/c/:name`} component={Community} />
<Route path={`/user/:id`} component={User} /> <Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} />
<Route path={`/u/:username`} component={User} /> <Route path={`/user/:id`} component={User} />
<Route path={`/inbox`} component={Inbox} /> <Route path={`/u/:username`} component={User} />
<Route path={`/modlog/community/:community_id`} component={Modlog} /> <Route path={`/inbox`} component={Inbox} />
<Route path={`/modlog`} component={Modlog} /> <Route path={`/modlog/community/:community_id`} component={Modlog} />
<Route path={`/setup`} component={Setup} /> <Route path={`/modlog`} component={Modlog} />
<Route path={`/search`} component={Search} /> <Route path={`/setup`} component={Setup} />
<Route path={`/sponsors`} component={Sponsors} /> <Route path={`/search`} component={Search} />
</Switch> <Route path={`/sponsors`} component={Sponsors} />
<Symbols /> </Switch>
</div> <Symbols />
<Footer /> </div>
</BrowserRouter> <Footer />
</BrowserRouter>
</Provider>
); );
} }

View file

@ -4,6 +4,7 @@ import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserService } from './'; import { UserService } from './';
import { i18n } from '../i18next';
export class WebSocketService { export class WebSocketService {
private static _instance: WebSocketService; private static _instance: WebSocketService;
@ -192,7 +193,7 @@ export class WebSocketService {
private setAuth(obj: any, throwErr: boolean = true) { private setAuth(obj: any, throwErr: boolean = true) {
obj.auth = UserService.Instance.auth; obj.auth = UserService.Instance.auth;
if (obj.auth == null && throwErr) { if (obj.auth == null && throwErr) {
alert("Not logged in."); alert(i18n.t('not_logged_in'));
throw "Not logged in"; throw "Not logged in";
} }
} }

124
ui/src/translations/de.ts Normal file
View file

@ -0,0 +1,124 @@
export const de = {
translation: {
post: 'post',
remove_post: 'Remove Post',
no_posts: 'No Posts.',
create_a_post: 'Create a post',
create_post: 'Create Post',
number_of_posts:'{{count}} Posts',
posts: 'Posts',
related_posts: 'These posts might be related',
comments: 'Comments',
number_of_comments:'{{count}} Comments',
remove_comment: 'Remove Comment',
communities: 'Communities',
create_a_community: 'Create a community',
create_community: 'Create Community',
remove_community: 'Remove Community',
subscribed_to_communities:'Subscribed to <1>communities</1>',
trending_communities:'Trending <1>communities</1>',
list_of_communities: 'List of communities',
community_reqs: 'lowercase, underscores, and no spaces.',
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
unlock: 'unlock',
lock: 'lock',
link: 'link',
mod: 'mod',
mods: 'mods',
moderates: 'Moderates',
remove_as_mod: 'remove as mod',
appoint_as_mod: 'appoint as mod',
modlog: 'Modlog',
admin: 'admin',
admins: 'admins',
remove_as_admin: 'remove as admin',
appoint_as_admin: 'appoint as admin',
remove: 'remove',
removed: 'removed',
locked: 'locked',
reason: 'Reason',
mark_as_read: 'mark as read',
mark_as_unread: 'mark as unread',
delete: 'delete',
deleted: 'deleted',
restore: 'restore',
ban: 'ban',
ban_from_site: 'ban from site',
unban: 'unban',
unban_from_site: 'unban from site',
save: 'save',
unsave: 'unsave',
create: 'create',
username: 'Username',
email_or_username: 'Email or Username',
number_of_users:'{{count}} Users',
number_of_subscribers:'{{count}} Subscribers',
number_of_points:'{{count}} Points',
name: 'Name',
title: 'Title',
category: 'Category',
subscribers: 'Subscribers',
both: 'Both',
saved: 'Saved',
unsubscribe: 'Unsubscribe',
subscribe: 'Subscribe',
prev: 'Prev',
next: 'Next',
sidebar: 'Sidebar',
sort_type: 'Sort type',
hot: 'Hot',
new: 'New',
top_day: 'Top day',
week: 'Week',
month: 'Month',
year: 'Year',
all: 'All',
top: 'Top',
api: 'API',
inbox: 'Inbox',
inbox_for: 'Inbox for <1>{{user}}</1>',
mark_all_as_read: 'mark all as read',
type: 'Type',
unread: 'Unread',
reply_sent: 'Reply sent',
search: 'Search',
overview: 'Overview',
view: 'View',
logout: 'Logout',
login_sign_up: 'Login / Sign up',
notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.',
unread_messages: 'Unread Messages',
password: 'Password',
verify_password: 'Verify Password',
login: 'Login',
sign_up: 'Sign Up',
email: 'Email',
optional: 'Optional',
url: 'URL',
body: 'Body',
copy_suggested_title: 'copy suggested title: {{title}}',
community: 'Community',
expand_here: 'Expand here',
subscribe_to_communities: 'Subscribe to some <1>communities</1>.',
chat: 'Chat',
no_results: 'No results.',
setup: 'Setup',
lemmy_instance_setup: 'Lemmy Instance Setup',
setup_admin: 'Set Up Site Administrator',
your_site: 'your site',
modified: 'modified',
sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
support_on_patreon: 'Support on Patreon',
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
bitcoin: 'Bitcoin',
ethereum: 'Ethereum',
code: 'Code',
powered_by: 'Powered by',
landing_0: 'GERMAN Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>Its self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
},
}

160
ui/src/translations/en.ts Normal file
View file

@ -0,0 +1,160 @@
export const en = {
translation: {
post: 'post',
remove_post: 'Remove Post',
no_posts: 'No Posts.',
create_a_post: 'Create a post',
create_post: 'Create Post',
number_of_posts:'{{count}} Posts',
posts: 'Posts',
related_posts: 'These posts might be related',
comments: 'Comments',
number_of_comments:'{{count}} Comments',
remove_comment: 'Remove Comment',
communities: 'Communities',
create_a_community: 'Create a community',
create_community: 'Create Community',
remove_community: 'Remove Community',
subscribed_to_communities:'Subscribed to <1>communities</1>',
trending_communities:'Trending <1>communities</1>',
list_of_communities: 'List of communities',
community_reqs: 'lowercase, underscores, and no spaces.',
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
unlock: 'unlock',
lock: 'lock',
link: 'link',
mod: 'mod',
mods: 'mods',
moderates: 'Moderates',
remove_as_mod: 'remove as mod',
appoint_as_mod: 'appoint as mod',
modlog: 'Modlog',
admin: 'admin',
admins: 'admins',
remove_as_admin: 'remove as admin',
appoint_as_admin: 'appoint as admin',
remove: 'remove',
removed: 'removed',
locked: 'locked',
reason: 'Reason',
mark_as_read: 'mark as read',
mark_as_unread: 'mark as unread',
delete: 'delete',
deleted: 'deleted',
restore: 'restore',
ban: 'ban',
ban_from_site: 'ban from site',
unban: 'unban',
unban_from_site: 'unban from site',
save: 'save',
unsave: 'unsave',
create: 'create',
username: 'Username',
email_or_username: 'Email or Username',
number_of_users:'{{count}} Users',
number_of_subscribers:'{{count}} Subscribers',
number_of_points:'{{count}} Points',
name: 'Name',
title: 'Title',
category: 'Category',
subscribers: 'Subscribers',
both: 'Both',
saved: 'Saved',
unsubscribe: 'Unsubscribe',
subscribe: 'Subscribe',
prev: 'Prev',
next: 'Next',
sidebar: 'Sidebar',
sort_type: 'Sort type',
hot: 'Hot',
new: 'New',
top_day: 'Top day',
week: 'Week',
month: 'Month',
year: 'Year',
all: 'All',
top: 'Top',
api: 'API',
inbox: 'Inbox',
inbox_for: 'Inbox for <1>{{user}}</1>',
mark_all_as_read: 'mark all as read',
type: 'Type',
unread: 'Unread',
reply_sent: 'Reply sent',
search: 'Search',
overview: 'Overview',
view: 'View',
logout: 'Logout',
login_sign_up: 'Login / Sign up',
login: 'Login',
sign_up: 'Sign Up',
notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.',
unread_messages: 'Unread Messages',
password: 'Password',
verify_password: 'Verify Password',
email: 'Email',
optional: 'Optional',
expires: 'Expires',
url: 'URL',
body: 'Body',
copy_suggested_title: 'copy suggested title: {{title}}',
community: 'Community',
expand_here: 'Expand here',
subscribe_to_communities: 'Subscribe to some <1>communities</1>.',
chat: 'Chat',
no_results: 'No results.',
setup: 'Setup',
lemmy_instance_setup: 'Lemmy Instance Setup',
setup_admin: 'Set Up Site Administrator',
your_site: 'your site',
modified: 'modified',
sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
support_on_patreon: 'Support on Patreon',
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: 'Crypto',
bitcoin: 'Bitcoin',
ethereum: 'Ethereum',
code: 'Code',
joined: 'Joined',
powered_by: 'Powered by',
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>Its self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: 'Not logged in.',
community_ban: 'You have been banned from this community.',
site_ban: 'You have been banned from the site',
couldnt_create_comment: 'Couldn\'t create comment.',
couldnt_like_comment: 'Couldn\'t like comment.',
couldnt_update_comment: 'Couldn\'t update comment.',
couldnt_save_comment: 'Couldn\'t save comment.',
no_comment_edit_allowed: 'Not allowed to edit comment.',
no_post_edit_allowed: 'Not allowed to edit post.',
no_community_edit_allowed: 'Not allowed to edit community.',
couldnt_find_community: 'Couldn\'t find community.',
couldnt_update_community: 'Couldn\'t update Community.',
community_already_exists: 'Community already exists.',
community_moderator_already_exists: 'Community moderator already exists.',
community_follower_already_exists: 'Community follower already exists.',
community_user_already_banned: 'Community user already banned.',
couldnt_create_post: 'Couldn\'t create post.',
couldnt_like_post: 'Couldn\'t like post.',
couldnt_find_post: 'Couldn\'t find post.',
couldnt_get_posts: 'Couldn\'t get posts',
couldnt_update_post: 'Couldn\'t update post',
couldnt_save_post: 'Couldn\'t save post.',
no_slurs: 'No slurs.',
not_an_admin: 'Not an admin.',
site_already_exists: 'Site already exists.',
couldnt_update_site: 'Couldn\'t update site.',
couldnt_find_that_username_or_email: 'Couldn\'t find that username or email.',
password_incorrect: 'Password incorrect.',
passwords_dont_match: 'Passwords do not match.',
admin_already_created: 'Sorry, there\'s already an admin.',
user_already_exists: 'User already exists.',
couldnt_update_user: 'Couldn\'t update user.',
system_err_login: 'System error. Try logging out and back in.',
},
}

View file

@ -159,3 +159,7 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
if (callNow) func.apply(context, args); if (callNow) func.apply(context, args);
} }
} }
export function getLanguage() {
return (navigator.language || navigator.userLanguage);
}

View file

@ -2,7 +2,7 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/runtime@^7.1.2": "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1":
version "7.5.5" version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@ -16,6 +16,11 @@
dependencies: dependencies:
"@types/jquery" "*" "@types/jquery" "*"
"@types/i18next@^12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-12.1.0.tgz#7c3fd3dbe03f9531147033773bbd0ca4f474a180"
integrity sha512-qLyqTkp3ZKHsSoX8CNVYcTyTkxlm0aRCUpaUVetgkSlSpiNCdWryOgaYwgbO04tJIfLgBXPcy0tJ3Nl/RagllA==
"@types/jquery@*": "@types/jquery@*":
version "3.3.30" version "3.3.30"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.30.tgz#af4ad612d86d954d74664b2b0ec337a251fddb5b" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.30.tgz#af4ad612d86d954d74664b2b0ec337a251fddb5b"
@ -1169,6 +1174,13 @@ hoist-non-inferno-statics@^1.1.3:
resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35" resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35"
integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU= integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU=
html-parse-stringify2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
dependencies:
void-elements "^2.0.1"
http-errors@1.7.2: http-errors@1.7.2:
version "1.7.2" version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@ -1200,6 +1212,13 @@ http-signature@~1.2.0:
jsprim "^1.2.2" jsprim "^1.2.2"
sshpk "^1.7.0" sshpk "^1.7.0"
i18next@^17.0.9:
version "17.0.9"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-17.0.9.tgz#5f835e91a34fa5e7da1e5ae4c4586c81d7c4b17f"
integrity sha512-fCYpm3TDzcfPIPN3hmgvC/QJx17QHI+Ul88qbixwIrifN9nBmk2c2oVxVYSDxnV5FgBXZJJ0O4yBYiZ8v1bX2A==
dependencies:
"@babel/runtime" "^7.3.1"
iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4: iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -1219,6 +1238,31 @@ ignore-walk@^3.0.1:
dependencies: dependencies:
minimatch "^3.0.4" minimatch "^3.0.4"
inferno-clone-vnode@^7.1.12:
version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.2.1.tgz#ae978e6d1cfa07a1616a7b4ecf5ca2f4fe070d5d"
integrity sha512-52ksls/sKFfLLXQW8v7My5QqX2i/CedlQM2JzCtkKMo18FovDt52jHNhfmWAbY9svcyxEzPjZMofHL/LFd7aIA==
dependencies:
inferno "7.2.1"
inferno-create-element@^7.1.12:
version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.2.1.tgz#6327b7a2195e0b08fab43df702889504845271c0"
integrity sha512-FGnIre6jRfr34bUgPMYWzj5/WA3htX3TQUYGhTVtiaREVxTj952eGcAMvOp4W4V6n2iK1Zl/qcTjrUdD2G3WiQ==
dependencies:
inferno "7.2.1"
inferno-i18next@nimbusec-oss/inferno-i18next:
version "7.1.12"
resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/f8c1403e60be70141c558e36f12f22c106cb7463"
dependencies:
html-parse-stringify2 "^2.0.1"
inferno "^7.1.12"
inferno-clone-vnode "^7.1.12"
inferno-create-element "^7.1.12"
inferno-shared "^7.1.12"
inferno-vnode-flags "^7.1.12"
inferno-router@^7.0.1: inferno-router@^7.0.1:
version "7.2.1" version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.2.1.tgz#ebea346a31422ed141df7177fb0b5aeb06cf8fe3" resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-7.2.1.tgz#ebea346a31422ed141df7177fb0b5aeb06cf8fe3"
@ -1229,17 +1273,17 @@ inferno-router@^7.0.1:
inferno "7.2.1" inferno "7.2.1"
path-to-regexp-es6 "1.7.0" path-to-regexp-es6 "1.7.0"
inferno-shared@7.2.1: inferno-shared@7.2.1, inferno-shared@^7.1.12:
version "7.2.1" version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.2.1.tgz#7512d626e252a4e0e3ea28f0396a815651226ed6" resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.2.1.tgz#7512d626e252a4e0e3ea28f0396a815651226ed6"
integrity sha512-QSzHVcjAy38bQWmk1nrfNsrjdrWtxleojYYg00RyuF4K6s4KCPMEch5MD7C4fCydzeBMGcZUliSoUZXpm3DVwQ== integrity sha512-QSzHVcjAy38bQWmk1nrfNsrjdrWtxleojYYg00RyuF4K6s4KCPMEch5MD7C4fCydzeBMGcZUliSoUZXpm3DVwQ==
inferno-vnode-flags@7.2.1: inferno-vnode-flags@7.2.1, inferno-vnode-flags@^7.1.12:
version "7.2.1" version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.2.1.tgz#833c39a16116dce86430c0bb7fedbd054ee32790" resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.2.1.tgz#833c39a16116dce86430c0bb7fedbd054ee32790"
integrity sha512-xYK45KNhlsKZtW60b9ahF9eICK45NtUJDGZxwxBegW98/hdL7/TyUP0gARKd4vmrwxdgwbupU6VAXPVbv7Wwgw== integrity sha512-xYK45KNhlsKZtW60b9ahF9eICK45NtUJDGZxwxBegW98/hdL7/TyUP0gARKd4vmrwxdgwbupU6VAXPVbv7Wwgw==
inferno@7.2.1, inferno@^7.0.1: inferno@7.2.1, inferno@^7.0.1, inferno@^7.1.12:
version "7.2.1" version "7.2.1"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.2.1.tgz#d82c14a237a004335ed03dd44395a4e0fe0d3729" resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.2.1.tgz#d82c14a237a004335ed03dd44395a4e0fe0d3729"
integrity sha512-+HGUvismTfy1MDRkfOxbD8nriu+lmajo/Z1JQckuisJPMJpspzxBaR9sxaWpVytjexi0Pcrh194COso4t3gAIQ== integrity sha512-+HGUvismTfy1MDRkfOxbD8nriu+lmajo/Z1JQckuisJPMJpspzxBaR9sxaWpVytjexi0Pcrh194COso4t3gAIQ==
@ -2851,6 +2895,11 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
void-elements@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
watch@^1.0.1: watch@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"