]> Untitled Git - lemmy.git/commitdiff
Adding support for internationalization / i18n (#189)
authorDessalines <dessalines@users.noreply.github.com>
Sat, 10 Aug 2019 00:14:43 +0000 (17:14 -0700)
committerGitHub <noreply@github.com>
Sat, 10 Aug 2019 00:14:43 +0000 (17:14 -0700)
* Still not working

* Starting to work on internationalization

* Main done.

* i18n translations first pass.

* Localization testing mostly done.

* Second front end pass.

* Added a few more translations.

* Adding back end translations.

39 files changed:
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
ui/package.json
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/footer.tsx
ui/src/components/home.tsx [deleted file]
ui/src/components/inbox.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/modlog.tsx
ui/src/components/moment-time.tsx
ui/src/components/navbar.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/search.tsx
ui/src/components/setup.tsx
ui/src/components/sidebar.tsx
ui/src/components/site-form.tsx
ui/src/components/sponsors.tsx
ui/src/components/user.tsx
ui/src/i18next.ts [new file with mode: 0644]
ui/src/index.tsx
ui/src/services/WebSocketService.ts
ui/src/translations/de.ts [new file with mode: 0644]
ui/src/translations/en.ts [new file with mode: 0644]
ui/src/utils.ts
ui/yarn.lock

index ffd7da2ea4d3a14595a3eab980356855fe81e30e..19752d833c3c9c3f373d4266248a75586a8fc6db 100644 (file)
@@ -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"))?
         }
       };
     }
index be4bb41aa27c5f619bedd4f13c4b5124778fe4f8..fe225794233d2631420385939a1ea96395f1796f 100644 (file)
@@ -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"))?
         }
       };
     }
index a60107812013f7e5952d21872a5146de628f0761..df6ea852f8b31a510585a7831ca1636cd4fa2276 100644 (file)
@@ -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"))?
         }
       };
     }
index 31541168903e7da8da209c8e64459f7f2b9cbe45..08fefae45fc81545defb47cdcb6f48b9a0591e3d 100644 (file)
@@ -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"))?
       }
     };
 
index d6d5962eb7ec4eada2943fc69e286c7c2ff80cf1..5d5f1a6be22d872ac4a1ae273e1c8626a4e758be 100644 (file)
@@ -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"))?
         }
       };
     }
index 20ecdd82aa5a02e2cae1d828e4150228c0ec59b0..d86725f254d45b35a846d9a5bf197820828cdc26 100644 (file)
@@ -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",
index 5181e45e121a6a3c67d5fd8c508f4e83c7eb2fee..ed62fcf5c1a558c7d01fda23fb28cf5090fd2a2e 100644 (file)
@@ -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'));
   }
 
index a201ddd6bef3de6e83d89efe713b3619085577fc..a1ac93b3807c088407b9020f548863aab1de0526 100644 (file)
@@ -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,
index da67bbc7f395e1bb64d3e6132b070f085ad41dde..fca323e395b78e6cc4e9a77fbe2158c6ef756864 100644 (file)
@@ -32,7 +32,7 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
             moderators={this.props.moderators}
             admins={this.props.admins}
             markable={this.props.markable}
-            />
+          />
         )}
       </div>
     )
index c4efe1fbebc12a7f88000e557502877038847929..49b982dc95df564674ad980d1ebda18d9125638e 100644 (file)
@@ -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;
index e295dcbedd340a9588107931efaaa529e2204c0a..b039fb4d9ee5d4d6fadeee6cd471a71ef6b39d1b 100644 (file)
@@ -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;
index 6a1f5da2b456fc3112ce686a6900e0dd37b9993f..480b909ea3b04ae519e6c1f79ccc54ab351f787d 100644 (file)
@@ -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;
index c2f89eef3311a5a7215124a8f8487db515cf13a7..61245e739f659fdc2d38ebbe1e74c5d01538eb5f 100644 (file)
@@ -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>
index e09bcf703443f36fbe7a6da18c585d56907931da..dd93a3c53797b002d0e5c6d38390e8022b6728e7 100644 (file)
@@ -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>
index 31403d7cad286dbbf4ab721119f218d60dcf01b1..87d7097e0394688d860556a9441e18b7126f20e7 100644 (file)
@@ -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 (file)
index e69de29..0000000
index 5fb7f874b492d13461e0640bd41815edb06d1a43..c9f46b36af6521c97a59e9ccb15acf010cf36115 100644 (file)
@@ -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) {
index 6eb88438df97c959842842e6ac17e4fa07eccf34..e7af89ca981133cd93be790fa3b3935eb90428df 100644 (file)
@@ -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;
index fe59ac2c2b8915ca20ac823d9ba54e2a0f1735b6..91d56cc0c2ced0f09b09f2e51f6587afe45d4ed6 100644 (file)
@@ -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,26 +122,38 @@ export class Main extends Component<any, MainState> {
             {this.posts()}
           </div>
           <div class="col-12 col-md-4">
-            {!this.state.loading &&
+            {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>
-                {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()}
+                <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>
             }
-          </div>
-        </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>
     )
   }
@@ -147,7 +161,9 @@ export class Main extends Component<any, MainState> {
   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;
index b8e5846159b5e4dfc71a3bbb745cef706cd5e461..ba1fe5a21d49cb0f3885831664ddc77219203ef5 100644 (file)
@@ -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;
index c882669530e30103b8055e02a911f34bab776bd3..021cf5f7606fe446a5638634f9a49b65abb06e51 100644 (file)
@@ -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_;
index 68e486c1f195a38c46754cac80fbae067be4c325..5738483db4c6aec043dbaf0f5ba29446748bebfc 100644 (file)
@@ -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}`
       });
index 54b3ca44004bcea88904c14d46584043b2dc80d9..8aa7a5eaa2de67937a689b97a28e300b3a889cc3 100644 (file)
@@ -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;
index 6727dd09c2f46351e20708a24c6eff2a64b21d4a..ff70783cb9237027a9a9731642d408821654bc66 100644 (file)
@@ -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)} />}
index 93b2f606f9dd1a3cec8e8e473f34a1c406b611e3..f5682a7e96ad88309ee8b0b814ab4d90dcdbead2 100644 (file)
@@ -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>
     )
index 7152941f1f8e5acf39a2594ff7066dd9085f7909..b0204d38844f8d20be5dd93ee2845874eed1e279 100644 (file)
@@ -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;
index ec657bb15fc20f95816e3e886563ad30b1716eb4..01122fd437373a7861977fe0dadb33ee39d2e010 100644 (file)
@@ -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);
     }
index edb98260c67f891e1795ab7df202b76712bafd70..f11dc14e0d07b64a238b8d527b86258bc454ef19 100644 (file)
@@ -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;
index d36d962c1c4a00466959f69fd7285b933316dd21..8d804343da927673d4b5776c62fef47a541aac80 100644 (file)
@@ -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 && 
index 7c51be40358731b8ac0ff35464ed6a383fa990fa..011642158e6cb2bd7ead54f27fb868f881c14109 100644 (file)
@@ -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>
index c0b36e4cc95c4d372f523148465dd65f4c332cde..3fd55c2fba2b2d09226fbe0f7e6027426071bcf0 100644 (file)
@@ -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>
index d7c2bf66de6ebd260330ff9a8eeb1c805e091831..c6a70560f497b74eeccbbe2f0795b12866fbf51d 100644 (file)
@@ -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 (file)
index 0000000..3b2ad60
--- /dev/null
@@ -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 };
index a50bf2a008db68d91385cee80752c54a89287582..41381513d05b5f3ae32aa4dd3f412d4a554fce2f 100644 (file)
@@ -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>
     );
   }
 
index 986855a347a4f9355fa10b1e4445f098986902c9..c192c2b77add21af3462149398dad568674f874d 100644 (file)
@@ -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 (file)
index 0000000..543d74d
--- /dev/null
@@ -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 (file)
index 0000000..619aced
--- /dev/null
@@ -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.',
+  },
+}
+
index b9d9a38994d25262453ac71a266d42646af564f4..c48b00c690f1806e1131a538437c537ea5267edc 100644 (file)
@@ -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);
+}
index c978ef94fd3adc7b52195edf53a8b5281e522811..f47c16c45912d5edf90adf73f97c51835d656f7d 100644 (file)
@@ -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==
   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"