diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index ffd7da2ea..19752d833 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -53,7 +53,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, 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 let post = Post::read(&conn, data.post_id)?; 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 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()); @@ -86,7 +86,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> { let inserted_comment = match Comment::create(&conn, &comment_form) { Ok(comment) => comment, 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) { Ok(like) => like, 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) { Ok(claims) => claims.claims, 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) { - 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 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 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) { Ok(comment) => comment, 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) { Ok(claims) => claims.claims, 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) { Ok(comment) => comment, Err(_e) => { - return Err(APIError::err(&self.op, "Couldnt do comment save"))? + return Err(APIError::err(&self.op, "couldnt_save_comment"))? } }; } else { match CommentSaved::unsave(&conn, &comment_saved_form) { Ok(comment) => comment, 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) { Ok(claims) => claims.claims, 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 let post = Post::read(&conn, data.post_id)?; 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 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 { @@ -299,7 +299,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> { let _inserted_like = match CommentLike::like(&conn, &like_form) { Ok(like) => like, Err(_e) => { - return Err(APIError::err(&self.op, "Couldn't like comment."))? + return Err(APIError::err(&self.op, "couldnt_like_comment"))? } }; } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index be4bb41aa..fe2257942 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -135,14 +135,14 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> { let community_view = match CommunityView::read(&conn, community_id, user_id) { Ok(community) => community, 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) { Ok(moderators) => moderators, 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) { Ok(claims) => claims.claims, 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) || has_slurs(&data.title) || (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; // Check for a site ban 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 @@ -200,7 +200,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> { let inserted_community = match Community::create(&conn, &community_form) { Ok(community) => community, 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) { Ok(user) => user, 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) { Ok(user) => user, 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; 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(); @@ -252,7 +252,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, 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 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 @@ -280,7 +280,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> { .collect() ); 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 { @@ -297,7 +297,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> { let _updated_community = match Community::update(&conn, data.edit_id, &community_form) { Ok(community) => community, 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) { Ok(claims) => claims.claims, 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) { Ok(user) => user, Err(_e) => { - return Err(APIError::err(&self.op, "Community follower already exists."))? + return Err(APIError::err(&self.op, "community_follower_already_exists"))? } }; } else { match CommunityFollower::ignore(&conn, &community_follower_form) { Ok(user) => user, 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) { Ok(claims) => claims.claims, 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) { Ok(communities) => communities, 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) { Ok(claims) => claims.claims, 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) { Ok(user) => user, Err(_e) => { - return Err(APIError::err(&self.op, "Community user ban already exists"))? + return Err(APIError::err(&self.op, "community_user_already_banned"))? } }; } else { match CommunityUserBan::unban(&conn, &community_user_ban_form) { Ok(user) => user, 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) { Ok(claims) => claims.claims, 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) { Ok(user) => user, Err(_e) => { - return Err(APIError::err(&self.op, "Community moderator already exists."))? + return Err(APIError::err(&self.op, "community_moderator_already_exists"))? } }; } else { match CommunityModerator::leave(&conn, &community_moderator_form) { Ok(user) => user, Err(_e) => { - return Err(APIError::err(&self.op, "Community moderator already exists."))? + return Err(APIError::err(&self.op, "community_moderator_already_exists"))? } }; } diff --git a/server/src/api/post.rs b/server/src/api/post.rs index a60107812..df6ea852f 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -94,25 +94,25 @@ impl Perform<PostResponse> for Oper<CreatePost> { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, 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) || (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; // Check for a community ban 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 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 { @@ -130,7 +130,7 @@ impl Perform<PostResponse> for Oper<CreatePost> { let inserted_post = match Post::create(&conn, &post_form) { Ok(post) => post, 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) { Ok(like) => like, 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)) { Ok(post) => post, 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) { Ok(post) => post, 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) { Ok(posts) => posts, 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) { Ok(claims) => claims.claims, 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 let post = Post::read(&conn, data.post_id)?; 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 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 { @@ -302,7 +302,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> { let _inserted_like = match PostLike::like(&conn, &like_form) { Ok(like) => like, 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)) { Ok(post) => post, 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; if has_slurs(&data.name) || (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(); @@ -337,7 +337,7 @@ impl Perform<PostResponse> for Oper<EditPost> { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, 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() ); 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 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 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 { @@ -388,7 +388,7 @@ impl Perform<PostResponse> for Oper<EditPost> { let _updated_post = match Post::update(&conn, data.edit_id, &post_form) { Ok(post) => post, 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) { Ok(claims) => claims.claims, 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) { Ok(post) => post, Err(_e) => { - return Err(APIError::err(&self.op, "Couldnt do post save"))? + return Err(APIError::err(&self.op, "couldnt_save_post"))? } }; } else { match PostSaved::unsave(&conn, &post_saved_form) { Ok(post) => post, Err(_e) => { - return Err(APIError::err(&self.op, "Couldnt do post save"))? + return Err(APIError::err(&self.op, "couldnt_save_post"))? } }; } diff --git a/server/src/api/site.rs b/server/src/api/site.rs index 315411689..08fefae45 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -144,20 +144,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, 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) || (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; // Make sure user is an 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 { @@ -170,7 +170,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> { match Site::create(&conn, &site_form) { Ok(site) => site, 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) { Ok(claims) => claims.claims, 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) || (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; // Make sure user is an admin 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)?; @@ -222,7 +222,7 @@ impl Perform<SiteResponse> for Oper<EditSite> { match Site::update(&conn, 1, &site_form) { Ok(site) => site, Err(_e) => { - return Err(APIError::err(&self.op, "Couldn't update site."))? + return Err(APIError::err(&self.op, "couldnt_update_site"))? } }; diff --git a/server/src/api/user.rs b/server/src/api/user.rs index d6d5962eb..5d5f1a6be 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -102,13 +102,13 @@ impl Perform<LoginResponse> for Oper<Login> { // Fetch that username / email let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) { 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 let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "Password incorrect"))? + return Err(APIError::err(&self.op, "password_incorrect"))? } // Return the jwt @@ -129,16 +129,16 @@ impl Perform<LoginResponse> for Oper<Register> { // Make sure passwords match 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) { - return Err(APIError::err(&self.op, "No slurs"))? + return Err(APIError::err(&self.op, "no_slurs"))? } // Make sure there are no admins 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 @@ -157,7 +157,7 @@ impl Perform<LoginResponse> for Oper<Register> { let inserted_user = match User_::register(&conn, &user_form) { Ok(user) => user, 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) { Ok(user) => user, 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) { Ok(user) => user, 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) { Ok(claims) => claims.claims, 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 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)?; @@ -348,7 +348,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, 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) { Ok(claims) => claims.claims, 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 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)?; @@ -407,7 +407,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, 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) { Ok(claims) => claims.claims, 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) { Ok(claims) => claims.claims, 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) { Ok(comment) => comment, Err(_e) => { - return Err(APIError::err(&self.op, "Couldn't update Comment"))? + return Err(APIError::err(&self.op, "couldnt_update_comment"))? } }; } diff --git a/ui/package.json b/ui/package.json index 20ecdd82a..d86725f25 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,7 +23,9 @@ "autosize": "^4.0.2", "classcat": "^1.1.3", "dotenv": "^6.1.0", + "i18next": "^17.0.9", "inferno": "^7.0.1", + "inferno-i18next": "nimbusec-oss/inferno-i18next", "inferno-router": "^7.0.1", "js-cookie": "^2.2.0", "jwt-decode": "^2.2.0", @@ -35,6 +37,7 @@ "ws": "^7.0.0" }, "devDependencies": { + "@types/i18next": "^12.1.0", "fuse-box": "^3.1.3", "ts-transform-classcat": "^0.0.2", "ts-transform-inferno": "^4.0.2", diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index 5181e45e1..ed62fcf5c 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -1,7 +1,10 @@ import { Component, linkEvent } from 'inferno'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces'; +import { capitalizeFirstLetter } from '../utils'; import { WebSocketService, UserService } from '../services'; import * as autosize from 'autosize'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface CommentFormProps { 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, 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) { super(props, context); + this.state = this.emptyState; if (this.props.node) { @@ -62,7 +66,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { <div class="row"> <div class="col-sm-12"> <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> </form> @@ -84,7 +88,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { if (i.props.node) { i.props.onReplyCancel(); } - + autosize.update(document.querySelector('textarea')); } diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index a201ddd6b..a1ac93b38 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -7,6 +7,8 @@ import * as moment from 'moment'; import { MomentTime } from './moment-time'; import { CommentForm } from './comment-form'; import { CommentNodes } from './comment-nodes'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; 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> </li> {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 && - <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"> <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 && !this.state.collapsed && <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"> {UserService.Instance.user && !this.props.viewOnly && <> <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 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> {this.myComment && <> <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 className="list-inline-item"> <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> </li> </> @@ -123,8 +125,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { {this.canMod && <li className="list-inline-item"> {!this.props.node.comment.removed ? - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> } </li> } @@ -134,14 +136,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { {!this.isMod && <li className="list-inline-item"> {!this.props.node.comment.banned_from_community ? - <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}>ban</span> : - <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}>unban</span> + <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> : + <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span> } </li> } {!this.props.node.comment.banned_from_community && <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> } </> @@ -152,14 +154,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { {!this.isAdmin && <li className="list-inline-item"> {!this.props.node.comment.banned ? - <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban from site</span> : - <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban 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)}><T i18nKey="unban_from_site">#</T></span> } </li> } {!this.props.node.comment.banned && <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> } </> @@ -167,11 +169,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { </> } <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> {this.props.markable && <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> } </ul> @@ -180,23 +182,23 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { </div> {this.state.showRemoveDialog && <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)} /> - <button type="submit" class="btn btn-secondary">Remove Comment</button> + <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"><T i18nKey="remove_comment">#</T></button> </form> } {this.state.showBanDialog && <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <div class="form-group row"> - <label class="col-form-label">Reason</label> - <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> + <label class="col-form-label"><T i18nKey="reason">#</T></label> + <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> </div> {/* TODO hold off on expires until later */} {/* <div class="form-group row"> */} {/* <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 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> </form> } @@ -387,9 +389,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { handleModBanBothSubmit(i: CommentNode) { event.preventDefault(); - console.log(BanType[i.state.banType]); - console.log(i.props.node.comment.banned); - if (i.state.banType == BanType.Community) { let form: BanFromCommunityForm = { user_id: i.props.node.comment.creator_id, diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index da67bbc7f..fca323e39 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -32,7 +32,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState moderators={this.props.moderators} admins={this.props.admins} markable={this.props.markable} - /> + /> )} </div> ) diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index c4efe1fbe..49b982dc9 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -5,6 +5,8 @@ import { retryWhen, delay, take } from 'rxjs/operators'; import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces'; import { WebSocketService } from '../services'; import { msgOp } from '../utils'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; declare const Sortable: any; @@ -26,12 +28,12 @@ export class Communities extends Component<any, CommunitiesState> { super(props, context); this.state = this.emptyState; this.subscription = WebSocketService.Instance.subject - .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) - .subscribe( - (msg) => this.parseMessage(msg), + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), (err) => console.error(err), () => console.log('complete') - ); + ); this.refetch(); @@ -46,7 +48,7 @@ export class Communities extends Component<any, CommunitiesState> { } componentDidMount() { - document.title = `Communities - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('communities')} - ${WebSocketService.Instance.site.name}`; } // Necessary for back button for some reason @@ -64,17 +66,17 @@ export class Communities extends Component<any, CommunitiesState> { {this.state.loading ? <h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <div> - <h5>List of communities</h5> + <h5><T i18nKey="list_of_communities">#</T></h5> <div class="table-responsive"> <table id="community_table" class="table table-sm table-hover"> <thead class="pointer"> <tr> - <th>Name</th> - <th class="d-none d-lg-table-cell">Title</th> - <th>Category</th> - <th class="text-right">Subscribers</th> - <th class="text-right d-none d-lg-table-cell">Posts</th> - <th class="text-right d-none d-lg-table-cell">Comments</th> + <th><T i18nKey="name">#</T></th> + <th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th> + <th><T i18nKey="category">#</T></th> + <th class="text-right"><T i18nKey="subscribers">#</T></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"><T i18nKey="comments">#</T></th> <th></th> </tr> </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"> {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.handleSubscribe)}>Subscribe</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)}><T i18nKey="subscribe">#</T></span> } </td> </tr> @@ -109,9 +111,9 @@ export class Communities extends Component<any, CommunitiesState> { return ( <div class="mt-2"> {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> ); } @@ -165,7 +167,7 @@ export class Communities extends Component<any, CommunitiesState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.ListCommunities) { let res: ListCommunitiesResponse = msg; diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx index e295dcbed..b039fb4d9 100644 --- a/ui/src/components/community-form.tsx +++ b/ui/src/components/community-form.tsx @@ -3,8 +3,10 @@ import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces'; import { WebSocketService } from '../services'; -import { msgOp } from '../utils'; +import { msgOp, capitalizeFirstLetter } from '../utils'; import * as autosize from 'autosize'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; import { Community } from '../interfaces'; @@ -74,25 +76,25 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt return ( <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <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"> - <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 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"> <input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} /> </div> </div> <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"> <textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> </div> </div> <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"> <select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}> {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"> {this.state.loading ? <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : - this.props.community ? 'Save' : 'Create'}</button> - {this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</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)}><T i18nKey="cancel">#</T></button>} </div> </div> </form> @@ -153,7 +155,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt let op: UserOperation = msgOp(msg); console.log(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); this.state.loading = false; this.setState(this.state); return; @@ -169,8 +171,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt this.state.loading = false; this.props.onCreate(res.community); } - - // TODO is this necessary? + // TODO is ths necessary else if (op == UserOperation.EditCommunity) { let res: CommunityResponse = msg; this.state.loading = false; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 6a1f5da2b..480b909ea 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -6,6 +6,7 @@ import { WebSocketService } from '../services'; import { PostListings } from './post-listings'; import { Sidebar } from './sidebar'; import { msgOp, routeSortTypeToEnum, fetchLimit } from '../utils'; +import { T } from 'inferno-i18next'; interface State { community: CommunityI; @@ -102,7 +103,7 @@ export class Community extends Component<any, State> { <div class="col-12 col-md-8"> <h5>{this.state.community.title} {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> {this.selects()} @@ -126,15 +127,15 @@ export class Community extends Component<any, State> { return ( <div className="mb-2"> <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto"> - <option disabled>Sort Type</option> - <option value={SortType.Hot}>Hot</option> - <option value={SortType.New}>New</option> + <option disabled><T i18nKey="sort_type">#</T></option> + <option value={SortType.Hot}><T i18nKey="hot">#</T></option> + <option value={SortType.New}><T i18nKey="new">#</T></option> <option disabled>──────────</option> - <option value={SortType.TopDay}>Top Day</option> - <option value={SortType.TopWeek}>Week</option> - <option value={SortType.TopMonth}>Month</option> - <option value={SortType.TopYear}>Year</option> - <option value={SortType.TopAll}>All</option> + <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> + <option value={SortType.TopWeek}><T i18nKey="week">#</T></option> + <option value={SortType.TopMonth}><T i18nKey="month">#</T></option> + <option value={SortType.TopYear}><T i18nKey="year">#</T></option> + <option value={SortType.TopAll}><T i18nKey="all">#</T></option> </select> </div> ) @@ -144,9 +145,9 @@ export class Community extends Component<any, State> { return ( <div class="mt-2"> {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> ); } @@ -193,7 +194,7 @@ export class Community extends Component<any, State> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetCommunity) { let res: GetCommunityResponse = msg; diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx index c2f89eef3..61245e739 100644 --- a/ui/src/components/create-community.tsx +++ b/ui/src/components/create-community.tsx @@ -2,6 +2,8 @@ import { Component } from 'inferno'; import { CommunityForm } from './community-form'; import { Community } from '../interfaces'; import { WebSocketService } from '../services'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; export class CreateCommunity extends Component<any, any> { @@ -11,7 +13,7 @@ export class CreateCommunity extends Component<any, any> { } componentDidMount() { - document.title = `Create Community - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`; } render() { @@ -19,7 +21,7 @@ export class CreateCommunity extends Component<any, any> { <div class="container"> <div class="row"> <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}/> </div> </div> diff --git a/ui/src/components/create-post.tsx b/ui/src/components/create-post.tsx index e09bcf703..dd93a3c53 100644 --- a/ui/src/components/create-post.tsx +++ b/ui/src/components/create-post.tsx @@ -1,6 +1,8 @@ import { Component } from 'inferno'; import { PostForm } from './post-form'; import { WebSocketService } from '../services'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; export class CreatePost extends Component<any, any> { @@ -10,7 +12,7 @@ export class CreatePost extends Component<any, any> { } componentDidMount() { - document.title = `Create Post - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`; } render() { @@ -18,7 +20,7 @@ export class CreatePost extends Component<any, any> { <div class="container"> <div class="row"> <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} /> </div> </div> diff --git a/ui/src/components/footer.tsx b/ui/src/components/footer.tsx index 31403d7ca..87d7097e0 100644 --- a/ui/src/components/footer.tsx +++ b/ui/src/components/footer.tsx @@ -2,6 +2,7 @@ import { Component } from 'inferno'; import { Link } from 'inferno-router'; import { repoUrl } from '../utils'; import { version } from '../version'; +import { T } from 'inferno-i18next'; export class Footer extends Component<any, any> { @@ -19,16 +20,16 @@ export class Footer extends Component<any, any> { <span class="navbar-text">{version}</span> </li> <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 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 class="nav-item"> - <Link class="nav-link" to="/sponsors">Sponsors</Link> + <Link class="nav-link" to="/sponsors"><T i18nKey="sponsors">#</T></Link> </li> <li class="nav-item"> - <a class="nav-link" href={repoUrl}>Code</a> + <a class="nav-link" href={repoUrl}><T i18nKey="code">#</T></a> </li> </ul> </div> diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 5fb7f874b..c9f46b36a 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -6,6 +6,8 @@ import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, C import { WebSocketService, UserService } from '../services'; import { msgOp } from '../utils'; import { CommentNodes } from './comment-nodes'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; enum UnreadType { Unread, All @@ -49,7 +51,7 @@ export class Inbox extends Component<any, InboxState> { } 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() { @@ -59,12 +61,12 @@ export class Inbox extends Component<any, InboxState> { <div class="row"> <div class="col-12"> <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> {this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread && <ul class="list-inline mb-1 text-muted small font-weight-bold"> <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> </ul> } @@ -81,18 +83,18 @@ export class Inbox extends Component<any, InboxState> { return ( <div className="mb-2"> <select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto"> - <option disabled>Type</option> - <option value={UnreadType.Unread}>Unread</option> - <option value={UnreadType.All}>All</option> + <option disabled><T i18nKey="type">#</T></option> + <option value={UnreadType.Unread}><T i18nKey="unread">#</T></option> + <option value={UnreadType.All}><T i18nKey="all">#</T></option> </select> <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 value={SortType.New}>New</option> - <option value={SortType.TopDay}>Top Day</option> - <option value={SortType.TopWeek}>Week</option> - <option value={SortType.TopMonth}>Month</option> - <option value={SortType.TopYear}>Year</option> - <option value={SortType.TopAll}>All</option> + <option disabled><T i18nKey="sort_type">#</T></option> + <option value={SortType.New}><T i18nKey="new">#</T></option> + <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> + <option value={SortType.TopWeek}><T i18nKey="week">#</T></option> + <option value={SortType.TopMonth}><T i18nKey="month">#</T></option> + <option value={SortType.TopYear}><T i18nKey="year">#</T></option> + <option value={SortType.TopAll}><T i18nKey="all">#</T></option> </select> </div> ) @@ -113,9 +115,9 @@ export class Inbox extends Component<any, InboxState> { return ( <div class="mt-2"> {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> ); } @@ -164,7 +166,7 @@ export class Inbox extends Component<any, InboxState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) { let res: GetRepliesResponse = msg; @@ -196,7 +198,7 @@ export class Inbox extends Component<any, InboxState> { this.setState(this.state); } else if (op == UserOperation.CreateComment) { // let res: CommentResponse = msg; - alert('Reply sent'); + alert(i18n.t('reply_sent')); // this.state.replies.unshift(res.comment); // TODO do this right // this.setState(this.state); } else if (op == UserOperation.SaveComment) { diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index 6eb88438d..e7af89ca9 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -4,6 +4,8 @@ import { retryWhen, delay, take } from 'rxjs/operators'; import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { msgOp } from '../utils'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface State { loginForm: LoginForm; @@ -50,7 +52,7 @@ export class Login extends Component<any, State> { } componentDidMount() { - document.title = `Login - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`; } render() { @@ -74,13 +76,13 @@ export class Login extends Component<any, State> { <form onSubmit={linkEvent(this, this.handleLoginSubmit)}> <h5>Login</h5> <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"> <input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> </div> </div> <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"> <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required /> </div> @@ -88,38 +90,37 @@ export class Login extends Component<any, State> { <div class="form-group row"> <div class="col-sm-10"> <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> </form> - {/* Forgot your password or deleted your account? Reset your password. TODO */} </div> ); } registerForm() { return ( <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> - <h5>Sign Up</h5> + <h5><T i18nKey="sign_up">#</T></h5> <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"> <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 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"> - <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 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"> <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> </div> </div> <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"> <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> </div> @@ -127,7 +128,7 @@ export class Login extends Component<any, State> { <div class="form-group row"> <div class="col-sm-10"> <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> @@ -183,7 +184,7 @@ export class Login extends Component<any, State> { parseMessage(msg: any) { let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); this.state = this.emptyState; this.setState(this.state); return; diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index fe59ac2c2..91d56cc0c 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -7,6 +7,8 @@ import { WebSocketService, UserService } from '../services'; import { PostListings } from './post-listings'; import { SiteForm } from './site-form'; import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum } from '../utils'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface MainState { subscribedCommunities: Array<CommunityUser>; @@ -120,34 +122,48 @@ export class Main extends Component<any, MainState> { {this.posts()} </div> <div class="col-12 col-md-4"> - {!this.state.loading && - <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> - } + {this.my_sidebar()} </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() { return ( <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"> {this.state.trendingCommunities.map(community => <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 && <ul class="list-inline mb-1 text-muted small font-weight-bold"> <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> </ul> } <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">{this.state.site.site.number_of_posts} Posts</li> - <li className="list-inline-item badge badge-light">{this.state.site.site.number_of_comments} Comments</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_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T> + </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 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 => <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() { return ( <div> - <h5>Powered by - <svg class="icon mx-2"><use xlinkHref="#icon-mouse"></use></svg> - <a href={repoUrl}>Lemmy<sup>Beta</sup></a> + <h5> + <T i18nKey="powered_by" class="d-inline">#</T> + <svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg> + <a href={repoUrl}>Lemmy<sup>beta</sup></a> </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>Its self-hostable, has live-updating comment threads, and is tiny (<code>~80kB</code>). Federation into the ActivityPub network is on the roadmap.</p> - <p>This is a <b>very early beta version</b>, and a lot of features are currently broken or missing.</p> - <p>Suggest new features or report bugs <a href={repoUrl}>here.</a></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> + <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> + </T> + </p> </div> ) } @@ -257,7 +287,7 @@ export class Main extends Component<any, MainState> { onChange={linkEvent(this, this.handleTypeChange)} disabled={UserService.Instance.user == undefined} /> - Subscribed + {i18n.t('subscribed')} </label> <label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}> <input type="radio" @@ -265,19 +295,19 @@ export class Main extends Component<any, MainState> { checked={this.state.type_ == ListingType.All} onChange={linkEvent(this, this.handleTypeChange)} /> - All + {i18n.t('all')} </label> </div> <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 value={SortType.Hot}>Hot</option> - <option value={SortType.New}>New</option> + <option disabled><T i18nKey="sort_type">#</T></option> + <option value={SortType.Hot}><T i18nKey="hot">#</T></option> + <option value={SortType.New}><T i18nKey="new">#</T></option> <option disabled>──────────</option> - <option value={SortType.TopDay}>Top Day</option> - <option value={SortType.TopWeek}>Week</option> - <option value={SortType.TopMonth}>Month</option> - <option value={SortType.TopYear}>Year</option> - <option value={SortType.TopAll}>All</option> + <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> + <option value={SortType.TopWeek}><T i18nKey="week">#</T></option> + <option value={SortType.TopMonth}><T i18nKey="month">#</T></option> + <option value={SortType.TopYear}><T i18nKey="year">#</T></option> + <option value={SortType.TopAll}><T i18nKey="all">#</T></option> </select> </div> ) @@ -287,9 +317,9 @@ export class Main extends Component<any, MainState> { return ( <div class="mt-2"> {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> ); } @@ -352,7 +382,7 @@ export class Main extends Component<any, MainState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetFollowedCommunities) { let res: GetFollowedCommunitiesResponse = msg; diff --git a/ui/src/components/modlog.tsx b/ui/src/components/modlog.tsx index b8e584615..ba1fe5a21 100644 --- a/ui/src/components/modlog.tsx +++ b/ui/src/components/modlog.tsx @@ -223,7 +223,7 @@ export class Modlog extends Component<any, ModlogState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetModlog) { let res: GetModlogResponse = msg; diff --git a/ui/src/components/moment-time.tsx b/ui/src/components/moment-time.tsx index c88266953..021cf5f76 100644 --- a/ui/src/components/moment-time.tsx +++ b/ui/src/components/moment-time.tsx @@ -1,5 +1,8 @@ import { Component } from 'inferno'; import * as moment from 'moment'; +// import 'moment/locale/de.js'; +import { getLanguage } from '../utils'; +import { i18n } from '../i18next'; interface MomentTimeProps { data: { @@ -13,12 +16,13 @@ export class MomentTime extends Component<MomentTimeProps, any> { constructor(props: any, context: any) { super(props, context); + moment.locale(getLanguage()); } render() { if (this.props.data.updated) { 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 { let str = this.props.data.published || this.props.data.when_; diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index 68e486c1f..5738483db 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -6,6 +6,8 @@ import { WebSocketService, UserService } from '../services'; import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces'; import { msgOp } from '../utils'; import { version } from '../version'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface NavbarState { isLoggedIn: boolean; @@ -85,16 +87,16 @@ export class Navbar extends Component<any, NavbarState> { <div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}> <ul class="navbar-nav mr-auto"> <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 class="nav-item"> - <Link class="nav-link" to="/search">Search</Link> + <Link class="nav-link" to="/search"><T i18nKey="search">#</T></Link> </li> <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 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> </ul> <ul class="navbar-nav ml-auto mr-2"> @@ -113,13 +115,13 @@ export class Navbar extends Component<any, NavbarState> { {UserService.Instance.user.username} </a> <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.handleLogoutClick) }>Logout</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) }><T i18nKey="logout">#</T></a> </div> </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> </div> @@ -153,6 +155,7 @@ export class Navbar extends Component<any, NavbarState> { parseMessage(msg: any) { let op: UserOperation = msgOp(msg); if (msg.error) { + // TODO if (msg.error == "Not logged in.") { UserService.Instance.logout(); location.reload(); @@ -209,7 +212,7 @@ export class Navbar extends Component<any, NavbarState> { if (UserService.Instance.user) { document.addEventListener('DOMContentLoaded', function () { if (!Notification) { - alert('Desktop notifications not available in your browser. Try Chromium.'); + alert(i18n.t('notifications_error')); return; } @@ -224,7 +227,7 @@ export class Navbar extends Component<any, NavbarState> { if (Notification.permission !== 'granted') Notification.requestPermission(); 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`, body: `${recentReply.creator_name}: ${recentReply.content}` }); diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 54b3ca440..8aa7a5eaa 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -4,8 +4,10 @@ import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { msgOp, getPageTitle, debounce } from '../utils'; +import { msgOp, getPageTitle, debounce, capitalizeFirstLetter } from '../utils'; import * as autosize from 'autosize'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface PostFormProps { 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> <form onSubmit={linkEvent(this, this.handlePostSubmit)}> <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"> <input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, debounce(this.handlePostUrlChange))} /> {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 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"> <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 && <> - <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} /> </> } </div> </div> <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"> <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} /> </div> @@ -114,7 +116,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> { {/* Cant change a community from an edit */} {!this.props.post && <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"> <select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}> {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"> {this.state.loading ? <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : - this.props.post ? 'Save' : 'Create'}</button> - {this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</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)}><T i18nKey="cancel">#</T></button>} </div> </div> </form> @@ -201,7 +203,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> { parseMessage(msg: any) { let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); this.state.loading = false; this.setState(this.state); return; diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index 6727dd09c..ff70783cb 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -5,6 +5,8 @@ import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, Communit import { MomentTime } from './moment-time'; import { PostForm } from './post-form'; import { mdToHtml, canMod, isMod, isImage } from '../utils'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface PostListingState { showEdit: boolean; @@ -67,14 +69,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> { </div> </div> {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> <h5 className="mb-0 d-inline"> {post.url ? <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> {post.url && @@ -83,18 +85,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> { </small> } {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 && - <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 && - <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) && <> { !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 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> <Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link> {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 && - <span className="mx-1 badge badge-light">admin</span> + <span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span> } {this.props.showCommunity && <span> @@ -137,22 +139,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> { </span> </li> <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> </ul> {UserService.Instance.user && this.props.editable && <ul class="list-inline mb-1 text-muted small font-weight-bold"> <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> {this.myPost && <> <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 className="list-inline-item mr-2"> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> - {!post.deleted ? 'delete' : 'restore'} + {!post.deleted ? i18n.t('delete') : i18n.t('restore')} </span> </li> </> @@ -161,12 +163,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> { <span> <li className="list-inline-item"> {!this.props.post.removed ? - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> } </li> <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> </span> } @@ -174,8 +176,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> { } {this.state.showRemoveDialog && <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)} /> - <button type="submit" class="btn btn-secondary">Remove Post</button> + <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"><T i18nKey="remove_post">#</T></button> </form> } {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />} diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index 93b2f606f..f5682a7e9 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -2,6 +2,7 @@ import { Component } from 'inferno'; import { Link } from 'inferno-router'; import { Post } from '../interfaces'; import { PostListing } from './post-listing'; +import { T } from 'inferno-i18next'; interface PostListingsProps { posts: Array<Post>; @@ -19,8 +20,10 @@ export class PostListings extends Component<PostListingsProps, any> { <div> {this.props.posts.length > 0 ? this.props.posts.map(post => <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> ) diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 7152941f1..b0204d388 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -9,6 +9,8 @@ import { Sidebar } from './sidebar'; import { CommentForm } from './comment-form'; import { CommentNodes } from './comment-nodes'; import * as autosize from 'autosize'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface PostState { post: PostI; @@ -130,17 +132,17 @@ export class Post extends Component<any, PostState> { sortRadios() { return ( <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} checked={this.state.commentSort === CommentSortType.Hot} onChange={linkEvent(this, this.handleCommentSortChange)} /> </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} checked={this.state.commentSort === CommentSortType.Top} onChange={linkEvent(this, this.handleCommentSortChange)} /> </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} checked={this.state.commentSort === CommentSortType.New} onChange={linkEvent(this, this.handleCommentSortChange)} /> @@ -152,7 +154,7 @@ export class Post extends Component<any, PostState> { newComments() { return ( <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} /> {this.state.comments.map(comment => <CommentNodes @@ -242,7 +244,7 @@ export class Post extends Component<any, PostState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetPost) { let res: GetPostResponse = msg; diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx index ec657bb15..01122fd43 100644 --- a/ui/src/components/search.tsx +++ b/ui/src/components/search.tsx @@ -6,6 +6,8 @@ import { WebSocketService } from '../services'; import { msgOp, fetchLimit } from '../utils'; import { PostListing } from './post-listing'; import { CommentNodes } from './comment-nodes'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface SearchState { q: string, @@ -52,7 +54,7 @@ export class Search extends Component<any, SearchState> { } componentDidMount() { - document.title = `Search - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`; } render() { @@ -60,7 +62,7 @@ export class Search extends Component<any, SearchState> { <div class="container"> <div class="row"> <div class="col-12"> - <h5>Search</h5> + <h5><T i18nKey="search">#</T></h5> {this.selects()} {this.searchForm()} {this.state.type_ == SearchType.Both && @@ -83,11 +85,11 @@ export class Search extends Component<any, SearchState> { searchForm() { return ( <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"> {this.state.loading ? <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : - <span>Search</span> + <span><T i18nKey="search">#</T></span> } </button> </form> @@ -98,19 +100,19 @@ export class Search extends Component<any, SearchState> { return ( <div className="mb-2"> <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto"> - <option disabled>Type</option> - <option value={SearchType.Both}>Both</option> - <option value={SearchType.Comments}>Comments</option> - <option value={SearchType.Posts}>Posts</option> + <option disabled><T i18nKey="type">#</T></option> + <option value={SearchType.Both}><T i18nKey="both">#</T></option> + <option value={SearchType.Comments}><T i18nKey="comments">#</T></option> + <option value={SearchType.Posts}><T i18nKey="posts">#</T></option> </select> <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 value={SortType.New}>New</option> - <option value={SortType.TopDay}>Top Day</option> - <option value={SortType.TopWeek}>Week</option> - <option value={SortType.TopMonth}>Month</option> - <option value={SortType.TopYear}>Year</option> - <option value={SortType.TopAll}>All</option> + <option disabled><T i18nKey="sort_type">#</T></option> + <option value={SortType.New}><T i18nKey="new">#</T></option> + <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> + <option value={SortType.TopWeek}><T i18nKey="week">#</T></option> + <option value={SortType.TopMonth}><T i18nKey="month">#</T></option> + <option value={SortType.TopYear}><T i18nKey="year">#</T></option> + <option value={SortType.TopAll}><T i18nKey="all">#</T></option> </select> </div> ) @@ -171,9 +173,9 @@ export class Search extends Component<any, SearchState> { return ( <div class="mt-2"> {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> ); } @@ -183,7 +185,7 @@ export class Search extends Component<any, SearchState> { return ( <div> {res && res.op && res.posts.length == 0 && res.comments.length == 0 && - <span>No Results</span> + <span><T i18nKey="no_results">#</T></span> } </div> ) @@ -244,13 +246,13 @@ export class Search extends Component<any, SearchState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.Search) { let res: SearchResponse = msg; this.state.searchResponse = res; 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); this.setState(this.state); } diff --git a/ui/src/components/setup.tsx b/ui/src/components/setup.tsx index edb98260c..f11dc14e0 100644 --- a/ui/src/components/setup.tsx +++ b/ui/src/components/setup.tsx @@ -5,6 +5,8 @@ import { RegisterForm, LoginResponse, UserOperation } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { msgOp } from '../utils'; import { SiteForm } from './site-form'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface State { userForm: RegisterForm; @@ -46,7 +48,7 @@ export class Setup extends Component<any, State> { } componentDidMount() { - document.title = "Setup - Lemmy"; + document.title = `${i18n.t('setup')} - Lemmy`; } render() { @@ -54,7 +56,7 @@ export class Setup extends Component<any, State> { <div class="container"> <div class="row"> <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 />} </div> </div> @@ -65,27 +67,27 @@ export class Setup extends Component<any, State> { registerUser() { return ( <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> - <h5>Set up Site Administrator</h5> + <h5><T i18nKey="setup_admin">#</T></h5> <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"> <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 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"> - <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 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"> <input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> </div> </div> <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"> <input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> </div> @@ -93,7 +95,7 @@ export class Setup extends Component<any, State> { <div class="form-group row"> <div class="col-sm-10"> <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> @@ -133,7 +135,7 @@ export class Setup extends Component<any, State> { parseMessage(msg: any) { let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); this.state.userLoading = false; this.setState(this.state); return; diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index d36d962c1..8d804343d 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -4,6 +4,8 @@ import { Community, CommunityUser, FollowCommunityForm, CommunityForm as Communi import { WebSocketService, UserService } from '../services'; import { mdToHtml, getUnixTime } from '../utils'; import { CommunityForm } from './community-form'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface SidebarProps { community: Community; @@ -54,10 +56,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> { <div> <h5 className="mb-0">{community.title} {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 && - <small className="ml-2 text-muted font-italic">deleted</small> + <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small> } </h5> <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 && <> <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> {this.amCreator && <li className="list-inline-item"> <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> - {!community.deleted ? 'delete' : 'restore'} + {!community.deleted ? i18n.t('delete') : i18n.t('restore')} </span> </li> } @@ -79,8 +81,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> { {this.canAdmin && <li className="list-inline-item"> {!this.props.community.removed ? - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : - <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : + <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> } </li> @@ -89,38 +91,38 @@ export class Sidebar extends Component<SidebarProps, SidebarState> { {this.state.showRemoveDialog && <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <div class="form-group row"> - <label class="col-form-label">Reason</label> - <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> + <label class="col-form-label"><T i18nKey="reason">#</T></label> + <input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> </div> {/* TODO hold off on expires for now */} {/* <div class="form-group row"> */} {/* <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 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> </form> } <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 badge badge-light">{community.number_of_subscribers} Subscribers</li> - <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li> - <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</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 badge badge-light"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></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"><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}`}><T i18nKey="modlog">#</T></Link></li> </ul> <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 => <li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li> )} </ul> <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> {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.handleSubscribe)}>Subscribe</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)}><T i18nKey="subscribe">#</T></button> } </div> {community.description && diff --git a/ui/src/components/site-form.tsx b/ui/src/components/site-form.tsx index 7c51be403..011642158 100644 --- a/ui/src/components/site-form.tsx +++ b/ui/src/components/site-form.tsx @@ -1,7 +1,10 @@ import { Component, linkEvent } from 'inferno'; import { Site, SiteForm as SiteFormI } from '../interfaces'; import { WebSocketService } from '../services'; +import { capitalizeFirstLetter } from '../utils'; import * as autosize from 'autosize'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface SiteFormProps { 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() { return ( <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"> - <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"> <input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} /> </div> </div> <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"> <textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> </div> @@ -57,8 +60,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { <button type="submit" class="btn btn-secondary mr-2"> {this.state.loading ? <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : - this.props.site ? 'Save' : 'Create'}</button> - {this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}>Cancel</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)}><T i18nKey="cancel">#</T></button>} </div> </div> </form> diff --git a/ui/src/components/sponsors.tsx b/ui/src/components/sponsors.tsx index c0b36e4cc..3fd55c2fb 100644 --- a/ui/src/components/sponsors.tsx +++ b/ui/src/components/sponsors.tsx @@ -1,5 +1,7 @@ import { Component } from 'inferno'; import { WebSocketService } from '../services'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; let general = [ @@ -18,7 +20,7 @@ export class Sponsors extends Component<any, any> { } componentDidMount() { - document.title = `Sponsors - ${WebSocketService.Instance.site.name}`; + document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`; } render() { @@ -36,19 +38,19 @@ export class Sponsors extends Component<any, any> { topMessage() { return ( <div> - <h5>Sponsors of Lemmy</h5> + <h5><T i18nKey="sponsors_of_lemmy">#</T></h5> <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> - <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> ) } sponsors() { return ( <div class="container"> - <h5>Sponsors</h5> - <p>General Sponsors are those that pledged $10 to $39 to Lemmy.</p> + <h5><T i18nKey="sponsors">#</T></h5> + <p><T i18nKey="general_sponsors">#</T></p> <div class="row card-columns"> {general.map(s => <div class="card col-12 col-md-2"> @@ -63,16 +65,16 @@ export class Sponsors extends Component<any, any> { bitcoin() { return ( <div> - <h5>Crypto</h5> + <h5><T i18nKey="crypto">#</T></h5> <div class="table-responsive"> <table class="table table-hover text-center"> <tbody> <tr> - <td>Bitcoin</td> + <td><T i18nKey="bitcoin">#</T></td> <td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td> </tr> <tr> - <td>Ethereum</td> + <td><T i18nKey="ethereum">#</T></td> <td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td> </tr> </tbody> diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index d7c2bf66d..c6a70560f 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -8,6 +8,8 @@ import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter } from '. import { PostListing } from './post-listing'; import { CommentNodes } from './comment-nodes'; import { MomentTime } from './moment-time'; +import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; enum View { Overview, Comments, Posts, Saved @@ -142,20 +144,20 @@ export class User extends Component<any, UserState> { return ( <div className="mb-2"> <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto"> - <option disabled>View</option> - <option value={View.Overview}>Overview</option> - <option value={View.Comments}>Comments</option> - <option value={View.Posts}>Posts</option> - <option value={View.Saved}>Saved</option> + <option disabled><T i18nKey="view">#</T></option> + <option value={View.Overview}><T i18nKey="overview">#</T></option> + <option value={View.Comments}><T i18nKey="comments">#</T></option> + <option value={View.Posts}><T i18nKey="posts">#</T></option> + <option value={View.Saved}><T i18nKey="saved">#</T></option> </select> <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 value={SortType.New}>New</option> - <option value={SortType.TopDay}>Top Day</option> - <option value={SortType.TopWeek}>Week</option> - <option value={SortType.TopMonth}>Month</option> - <option value={SortType.TopYear}>Year</option> - <option value={SortType.TopAll}>All</option> + <option disabled><T i18nKey="sort_type">#</T></option> + <option value={SortType.New}><T i18nKey="new">#</T></option> + <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> + <option value={SortType.TopWeek}><T i18nKey="week">#</T></option> + <option value={SortType.TopMonth}><T i18nKey="month">#</T></option> + <option value={SortType.TopYear}><T i18nKey="year">#</T></option> + <option value={SortType.TopAll}><T i18nKey="all">#</T></option> </select> </div> ) @@ -217,15 +219,15 @@ export class User extends Component<any, UserState> { return ( <div> <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"> <tr> - <td>{user.post_score} points</td> - <td>{user.number_of_posts} posts</td> + <td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td> + <td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td> </tr> <tr> - <td>{user.comment_score} points</td> - <td>{user.number_of_comments} comments</td> + <td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td> + <td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td> </tr> </table> <hr /> @@ -238,7 +240,7 @@ export class User extends Component<any, UserState> { <div> {this.state.moderates.length > 0 && <div> - <h5>Moderates</h5> + <h5><T i18nKey="moderates">#</T></h5> <ul class="list-unstyled"> {this.state.moderates.map(community => <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 && <div> <hr /> - <h5>Subscribed</h5> + <h5><T i18nKey="subscribed">#</T></h5> <ul class="list-unstyled"> {this.state.follows.map(community => <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> @@ -272,9 +274,9 @@ export class User extends Component<any, UserState> { return ( <div class="mt-2"> {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> ); } @@ -331,7 +333,7 @@ export class User extends Component<any, UserState> { console.log(msg); let op: UserOperation = msgOp(msg); if (msg.error) { - alert(msg.error); + alert(i18n.t(msg.error)); return; } else if (op == UserOperation.GetUserDetails) { let res: UserDetailsResponse = msg; @@ -359,7 +361,7 @@ export class User extends Component<any, UserState> { this.setState(this.state); } else if (op == UserOperation.CreateComment) { // let res: CommentResponse = msg; - alert('Reply sent'); + alert(i18n.t('reply_sent')); // this.state.comments.unshift(res.comment); // TODO do this right // this.setState(this.state); } else if (op == UserOperation.SaveComment) { diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts new file mode 100644 index 000000000..3b2ad6048 --- /dev/null +++ b/ui/src/i18next.ts @@ -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 }; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index a50bf2a00..41381513d 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,5 +1,6 @@ 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 { Navbar } from './components/navbar'; import { Footer } from './components/footer'; @@ -16,6 +17,7 @@ import { Inbox } from './components/inbox'; import { Search } from './components/search'; import { Sponsors } from './components/sponsors'; import { Symbols } from './components/symbols'; +import { i18n } from './i18next'; import './css/bootstrap.min.css'; import './css/main.css'; @@ -34,37 +36,39 @@ class Index extends Component<any, any> { render() { return ( - <BrowserRouter> - <Navbar /> - <div class="mt-1 p-0"> - <Switch> - <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} /> - <Route exact path={`/`} component={Main} /> - <Route path={`/login`} component={Login} /> - <Route path={`/create_post/c/:name`} component={CreatePost} /> - <Route path={`/create_post`} component={CreatePost} /> - <Route path={`/create_community`} component={CreateCommunity} /> - <Route path={`/communities/page/:page`} component={Communities} /> - <Route path={`/communities`} component={Communities} /> - <Route path={`/post/:id/comment/:comment_id`} component={Post} /> - <Route path={`/post/:id`} component={Post} /> - <Route path={`/c/:name/sort/:sort/page/:page`} component={Community} /> - <Route path={`/community/:id`} component={Community} /> - <Route path={`/c/:name`} component={Community} /> - <Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} /> - <Route path={`/user/:id`} component={User} /> - <Route path={`/u/:username`} component={User} /> - <Route path={`/inbox`} component={Inbox} /> - <Route path={`/modlog/community/:community_id`} component={Modlog} /> - <Route path={`/modlog`} component={Modlog} /> - <Route path={`/setup`} component={Setup} /> - <Route path={`/search`} component={Search} /> - <Route path={`/sponsors`} component={Sponsors} /> - </Switch> - <Symbols /> - </div> - <Footer /> - </BrowserRouter> + <Provider i18next={i18n}> + <BrowserRouter> + <Navbar /> + <div class="mt-1 p-0"> + <Switch> + <Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} /> + <Route exact path={`/`} component={Main} /> + <Route path={`/login`} component={Login} /> + <Route path={`/create_post/c/:name`} component={CreatePost} /> + <Route path={`/create_post`} component={CreatePost} /> + <Route path={`/create_community`} component={CreateCommunity} /> + <Route path={`/communities/page/:page`} component={Communities} /> + <Route path={`/communities`} component={Communities} /> + <Route path={`/post/:id/comment/:comment_id`} component={Post} /> + <Route path={`/post/:id`} component={Post} /> + <Route path={`/c/:name/sort/:sort/page/:page`} component={Community} /> + <Route path={`/community/:id`} component={Community} /> + <Route path={`/c/:name`} component={Community} /> + <Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} /> + <Route path={`/user/:id`} component={User} /> + <Route path={`/u/:username`} component={User} /> + <Route path={`/inbox`} component={Inbox} /> + <Route path={`/modlog/community/:community_id`} component={Modlog} /> + <Route path={`/modlog`} component={Modlog} /> + <Route path={`/setup`} component={Setup} /> + <Route path={`/search`} component={Search} /> + <Route path={`/sponsors`} component={Sponsors} /> + </Switch> + <Symbols /> + </div> + <Footer /> + </BrowserRouter> + </Provider> ); } diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 986855a34..c192c2b77 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -4,6 +4,7 @@ import { webSocket } from 'rxjs/webSocket'; import { Subject } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; import { UserService } from './'; +import { i18n } from '../i18next'; export class WebSocketService { private static _instance: WebSocketService; @@ -192,7 +193,7 @@ export class WebSocketService { private setAuth(obj: any, throwErr: boolean = true) { obj.auth = UserService.Instance.auth; if (obj.auth == null && throwErr) { - alert("Not logged in."); + alert(i18n.t('not_logged_in')); throw "Not logged in"; } } diff --git a/ui/src/translations/de.ts b/ui/src/translations/de.ts new file mode 100644 index 000000000..543d74dc7 --- /dev/null +++ b/ui/src/translations/de.ts @@ -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>.', + }, +} + diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts new file mode 100644 index 000000000..619aced69 --- /dev/null +++ b/ui/src/translations/en.ts @@ -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.', + }, +} + diff --git a/ui/src/utils.ts b/ui/src/utils.ts index b9d9a3899..c48b00c69 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -159,3 +159,7 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal if (callNow) func.apply(context, args); } } + +export function getLanguage() { + return (navigator.language || navigator.userLanguage); +} diff --git a/ui/yarn.lock b/ui/yarn.lock index c978ef94f..f47c16c45 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/runtime@^7.1.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== @@ -16,6 +16,11 @@ dependencies: "@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@*": version "3.3.30" 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" 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: version "1.7.2" 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" 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: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1219,6 +1238,31 @@ ignore-walk@^3.0.1: dependencies: 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: version "7.2.1" 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" 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" resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.2.1.tgz#7512d626e252a4e0e3ea28f0396a815651226ed6" 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" resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.2.1.tgz#833c39a16116dce86430c0bb7fedbd054ee32790" 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" resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.2.1.tgz#d82c14a237a004335ed03dd44395a4e0fe0d3729" integrity sha512-+HGUvismTfy1MDRkfOxbD8nriu+lmajo/Z1JQckuisJPMJpspzxBaR9sxaWpVytjexi0Pcrh194COso4t3gAIQ== @@ -2851,6 +2895,11 @@ verror@1.10.0: core-util-is "1.0.2" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"