From e7854c1ed8facc2955ef9ad96f0bb2513041bba9 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 16 Nov 2020 16:30:32 +0800
Subject: ...

---
 FrontEnd/src/app/views/admin/Admin.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'FrontEnd/src/app/views/admin/Admin.tsx')

diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx
index 9c0250e7..a64a9bc0 100644
--- a/FrontEnd/src/app/views/admin/Admin.tsx
+++ b/FrontEnd/src/app/views/admin/Admin.tsx
@@ -8,12 +8,12 @@ import {
 } from "react-router";
 import { Nav } from "react-bootstrap";
 
-import { UserWithToken } from "@/services/user";
+import { AuthUser } from "@/services/user";
 
 import UserAdmin from "./UserAdmin";
 
 interface AdminProps {
-  user: UserWithToken;
+  user: AuthUser;
 }
 
 const Admin: React.FC<AdminProps> = (props) => {
-- 
cgit v1.2.3


From 8f5ffab46b18eb30dfebeb8407435dd85dc35232 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 16 Nov 2020 17:20:42 +0800
Subject: ...

---
 FrontEnd/src/app/http/user.ts                 |  8 +++
 FrontEnd/src/app/views/admin/Admin.tsx        | 64 ++++-----------------
 FrontEnd/src/app/views/admin/AdminSubPage.tsx | 44 +++++++++++++++
 FrontEnd/src/app/views/admin/UserAdmin.tsx    | 80 ++++++---------------------
 4 files changed, 80 insertions(+), 116 deletions(-)
 create mode 100644 FrontEnd/src/app/views/admin/AdminSubPage.tsx

(limited to 'FrontEnd/src/app/views/admin/Admin.tsx')

diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts
index 9ba6508f..92a6433e 100644
--- a/FrontEnd/src/app/http/user.ts
+++ b/FrontEnd/src/app/http/user.ts
@@ -49,6 +49,7 @@ export class HttpChangePasswordBadCredentialError extends Error {
 }
 
 export interface IHttpUserClient {
+  list(): Promise<HttpUser[]>;
   get(username: string): Promise<HttpUser>;
   patch(
     username: string,
@@ -75,6 +76,13 @@ export interface IHttpUserClient {
 }
 
 export class HttpUserClient implements IHttpUserClient {
+  list(): Promise<HttpUser[]> {
+    return axios
+      .get<HttpUser[]>(`${apiBaseUrl}/users`)
+      .then(extractResponseData)
+      .catch(convertToNetworkError);
+  }
+
   get(username: string): Promise<HttpUser> {
     return axios
       .get<HttpUser>(`${apiBaseUrl}/users/${username}`)
diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx
index a64a9bc0..e17da5bc 100644
--- a/FrontEnd/src/app/views/admin/Admin.tsx
+++ b/FrontEnd/src/app/views/admin/Admin.tsx
@@ -1,12 +1,5 @@
 import React, { Fragment } from "react";
-import {
-  Redirect,
-  Route,
-  Switch,
-  useRouteMatch,
-  useHistory,
-} from "react-router";
-import { Nav } from "react-bootstrap";
+import { Redirect, Route, Switch, useRouteMatch, match } from "react-router";
 
 import { AuthUser } from "@/services/user";
 
@@ -16,57 +9,22 @@ interface AdminProps {
   user: AuthUser;
 }
 
-const Admin: React.FC<AdminProps> = (props) => {
+const Admin: React.FC<AdminProps> = ({ user }) => {
   const match = useRouteMatch();
-  const history = useHistory();
-  type TabNames = "users" | "more";
-
-  const tabName = history.location.pathname.replace(match.path + "/", "");
-
-  function toggle(newTab: TabNames): void {
-    history.push(`${match.url}/${newTab}`);
-  }
-
-  const createRoute = (
-    name: string,
-    body: React.ReactNode
-  ): React.ReactNode => {
-    return (
-      <Route path={`${match.path}/${name}`}>
-        <div style={{ height: 56 }} className="flex-fix-length" />
-        <Nav variant="tabs">
-          <Nav.Item>
-            <Nav.Link
-              active={tabName === "users"}
-              onClick={() => {
-                toggle("users");
-              }}
-            >
-              Users
-            </Nav.Link>
-          </Nav.Item>
-          <Nav.Item>
-            <Nav.Link
-              active={tabName === "more"}
-              onClick={() => {
-                toggle("more");
-              }}
-            >
-              More
-            </Nav.Link>
-          </Nav.Item>
-        </Nav>
-        {body}
-      </Route>
-    );
-  };
 
   return (
     <Fragment>
       <Switch>
         <Redirect from={match.path} to={`${match.path}/users`} exact />
-        {createRoute("users", <UserAdmin user={props.user} />)}
-        {createRoute("more", <div>More Page Works</div>)}
+        <Route path={`${match.path}/:name`}>
+          {(p) => {
+            const match = p.match as match<{ name: string }>;
+            const name = match.params["name"];
+            if (name === "users") {
+              return <UserAdmin user={user} />;
+            }
+          }}
+        </Route>
       </Switch>
     </Fragment>
   );
diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx
new file mode 100644
index 00000000..5d2df13c
--- /dev/null
+++ b/FrontEnd/src/app/views/admin/AdminSubPage.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { Nav } from "react-bootstrap";
+import { useHistory, useRouteMatch } from "react-router";
+
+const AdminSubPage: React.FC = ({ children }) => {
+  const match = useRouteMatch<{ name: string }>();
+  const history = useHistory();
+
+  const name = match.params.name;
+
+  function toggle(newTab: string): void {
+    history.push(`/admin/${newTab}`);
+  }
+
+  return (
+    <>
+      <Nav variant="tabs">
+        <Nav.Item>
+          <Nav.Link
+            active={name === "users"}
+            onClick={() => {
+              toggle("users");
+            }}
+          >
+            Users
+          </Nav.Link>
+        </Nav.Item>
+        <Nav.Item>
+          <Nav.Link
+            active={name === "more"}
+            onClick={() => {
+              toggle("more");
+            }}
+          >
+            More
+          </Nav.Link>
+        </Nav.Item>
+      </Nav>
+      {children}
+    </>
+  );
+};
+
+export default AdminSubPage;
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index 4ad9ed09..856e6136 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -10,19 +10,16 @@ import {
 } from "react-bootstrap";
 
 import OperationDialog from "../common/OperationDialog";
+import AdminSubPage from "./AdminSubPage";
+
 import { User, AuthUser } from "@/services/user";
+import { getHttpUserClient } from "@/http/user";
 
 const apiBaseUrl = "/api";
 
-async function fetchUserList(_token: string): Promise<User[]> {
-  const res = await axios.get<User[]>(`${apiBaseUrl}/users`);
-  return res.data;
-}
-
 interface CreateUserInfo {
   username: string;
   password: string;
-  administrator: boolean;
 }
 
 async function createUser(user: CreateUserInfo, token: string): Promise<User> {
@@ -57,16 +54,6 @@ function changePassword(
   });
 }
 
-function changePermission(
-  username: string,
-  newPermission: boolean,
-  token: string
-): Promise<void> {
-  return axios.patch(`${apiBaseUrl}/users/${username}?token=${token}`, {
-    administrator: newPermission,
-  });
-}
-
 const kChangeUsername = "changeusername";
 const kChangePassword = "changepassword";
 const kChangePermission = "changepermission";
@@ -103,9 +90,9 @@ const UserItem: React.FC<UserCardProps> = (props) => {
         <Col>
           <p className="mb-0 text-primary">{user.username}</p>
           <small
-            className={user.administrator ? "text-danger" : "text-secondary"}
+            className={user.permissions ? "text-danger" : "text-secondary"}
           >
-            {user.administrator ? "administrator" : "user"}
+            {user.permissions ? "administrator" : "user"}
           </small>
         </Col>
         <Col className="col-auto">
@@ -156,14 +143,12 @@ const CreateUserDialog: React.FC<CreateUserDialogProps> = (props) => {
         [
           { type: "text", label: "Username" },
           { type: "text", label: "Password" },
-          { type: "bool", label: "Administrator" },
         ] as const
       }
-      onProcess={([username, password, administrator]) =>
+      onProcess={([username, password]) =>
         props.process({
           username: username,
           password: password,
-          administrator: administrator,
         })
       }
       close={props.close}
@@ -316,15 +301,17 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
 
   useEffect(() => {
     let subscribe = true;
-    void fetchUserList(props.user.token).then((us) => {
-      if (subscribe) {
-        setUsers(us);
-      }
-    });
+    void getHttpUserClient()
+      .list()
+      .then((us) => {
+        if (subscribe) {
+          setUsers(us);
+        }
+      });
     return () => {
       subscribe = false;
     };
-  }, [props.user]);
+  }, []);
 
   let dialogNode: React.ReactNode;
   if (dialog)
@@ -389,26 +376,6 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
         );
         break;
       case kChangePermission: {
-        const newPermission = dialog.newPermission;
-        dialogNode = (
-          <UserChangePermissionDialog
-            open
-            close={() => setDialog(null)}
-            username={dialog.username}
-            newPermission={newPermission}
-            process={async () => {
-              await changePermission(dialog.username, newPermission, token);
-              setUsers((oldUsers) => {
-                const users = (oldUsers ?? []).slice();
-                const findedUser = users.find(
-                  (u) => u.username === dialog.username
-                );
-                if (findedUser) findedUser.administrator = newPermission;
-                return users;
-              });
-            }}
-          />
-        );
         break;
       }
     }
@@ -419,26 +386,13 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
         <UserItem
           key={user.username}
           user={user}
-          onContextMenu={(item) => {
-            setDialog(
-              item === kChangePermission
-                ? {
-                    type: kChangePermission,
-                    username: user.username,
-                    newPermission: !user.administrator,
-                  }
-                : {
-                    type: item,
-                    username: user.username,
-                  }
-            );
-          }}
+          onContextMenu={(item) => {}}
         />
       );
     });
 
     return (
-      <>
+      <AdminSubPage>
         <Button
           variant="success"
           onClick={() =>
@@ -452,7 +406,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
         </Button>
         {userComponents}
         {dialogNode}
-      </>
+      </AdminSubPage>
     );
   } else {
     return <Spinner animation="border" />;
-- 
cgit v1.2.3


From 1e6e30ec4bdf58b5b6bc19edf8c34f15656e9eb2 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Fri, 20 Nov 2020 18:31:15 +0800
Subject: ...

---
 FrontEnd/src/app/index.sass                        |  2 ++
 FrontEnd/src/app/views/admin/Admin.tsx             | 18 ++++++++--
 FrontEnd/src/app/views/admin/AdminNav.tsx          | 41 ++++++++++++++++++++++
 FrontEnd/src/app/views/admin/AdminSubPage.tsx      | 34 ------------------
 .../src/app/views/admin/HighlightTimelineAdmin.tsx | 13 +++++++
 FrontEnd/src/app/views/admin/UserAdmin.tsx         |  5 ++-
 FrontEnd/src/app/views/admin/admin.sass            |  0
 7 files changed, 73 insertions(+), 40 deletions(-)
 create mode 100644 FrontEnd/src/app/views/admin/AdminNav.tsx
 delete mode 100644 FrontEnd/src/app/views/admin/AdminSubPage.tsx
 create mode 100644 FrontEnd/src/app/views/admin/HighlightTimelineAdmin.tsx
 create mode 100644 FrontEnd/src/app/views/admin/admin.sass

(limited to 'FrontEnd/src/app/views/admin/Admin.tsx')

diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass
index b8cc464e..e3c1c5b9 100644
--- a/FrontEnd/src/app/index.sass
+++ b/FrontEnd/src/app/index.sass
@@ -10,6 +10,8 @@
 @import './views/timeline/timeline'
 @import './views/user/user'
 
+@import './views/admin/admin'
+
 body
   margin: 0
 
diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx
index e17da5bc..ac640700 100644
--- a/FrontEnd/src/app/views/admin/Admin.tsx
+++ b/FrontEnd/src/app/views/admin/Admin.tsx
@@ -1,9 +1,12 @@
 import React, { Fragment } from "react";
 import { Redirect, Route, Switch, useRouteMatch, match } from "react-router";
+import { Container } from "react-bootstrap";
 
 import { AuthUser } from "@/services/user";
 
+import AdminNav from "./AdminNav";
 import UserAdmin from "./UserAdmin";
+import HighlightTimelineAdmin from "./HighlightTimelineAdmin";
 
 interface AdminProps {
   user: AuthUser;
@@ -20,9 +23,18 @@ const Admin: React.FC<AdminProps> = ({ user }) => {
           {(p) => {
             const match = p.match as match<{ name: string }>;
             const name = match.params["name"];
-            if (name === "users") {
-              return <UserAdmin user={user} />;
-            }
+            return (
+              <Container>
+                <AdminNav />
+                {(() => {
+                  if (name === "users") {
+                    return <UserAdmin user={user} />;
+                  } else if (name === "highlighttimelines") {
+                    return <HighlightTimelineAdmin user={user} />;
+                  }
+                })()}
+              </Container>
+            );
           }}
         </Route>
       </Switch>
diff --git a/FrontEnd/src/app/views/admin/AdminNav.tsx b/FrontEnd/src/app/views/admin/AdminNav.tsx
new file mode 100644
index 00000000..040b479f
--- /dev/null
+++ b/FrontEnd/src/app/views/admin/AdminNav.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Nav } from "react-bootstrap";
+import { useHistory, useRouteMatch } from "react-router";
+
+const AdminNav: React.FC = () => {
+  const match = useRouteMatch<{ name: string }>();
+  const history = useHistory();
+
+  const name = match.params.name;
+
+  function toggle(newTab: string): void {
+    history.push(`/admin/${newTab}`);
+  }
+
+  return (
+    <Nav variant="tabs" className="my-2">
+      <Nav.Item>
+        <Nav.Link
+          active={name === "users"}
+          onClick={() => {
+            toggle("users");
+          }}
+        >
+          Users
+        </Nav.Link>
+      </Nav.Item>
+      <Nav.Item>
+        <Nav.Link
+          active={name === "highlighttimelines"}
+          onClick={() => {
+            toggle("highlighttimelines");
+          }}
+        >
+          Highlight Timelines
+        </Nav.Link>
+      </Nav.Item>
+    </Nav>
+  );
+};
+
+export default AdminNav;
diff --git a/FrontEnd/src/app/views/admin/AdminSubPage.tsx b/FrontEnd/src/app/views/admin/AdminSubPage.tsx
deleted file mode 100644
index 5efe1ee3..00000000
--- a/FrontEnd/src/app/views/admin/AdminSubPage.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from "react";
-import { Container, Nav } from "react-bootstrap";
-import { useHistory, useRouteMatch } from "react-router";
-
-const AdminSubPage: React.FC = ({ children }) => {
-  const match = useRouteMatch<{ name: string }>();
-  const history = useHistory();
-
-  const name = match.params.name;
-
-  function toggle(newTab: string): void {
-    history.push(`/admin/${newTab}`);
-  }
-
-  return (
-    <Container>
-      <Nav variant="tabs" className="my-2">
-        <Nav.Item>
-          <Nav.Link
-            active={name === "users"}
-            onClick={() => {
-              toggle("users");
-            }}
-          >
-            Users
-          </Nav.Link>
-        </Nav.Item>
-      </Nav>
-      {children}
-    </Container>
-  );
-};
-
-export default AdminSubPage;
diff --git a/FrontEnd/src/app/views/admin/HighlightTimelineAdmin.tsx b/FrontEnd/src/app/views/admin/HighlightTimelineAdmin.tsx
new file mode 100644
index 00000000..3de7d5a6
--- /dev/null
+++ b/FrontEnd/src/app/views/admin/HighlightTimelineAdmin.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+import { AuthUser } from "@/services/user";
+
+export interface HighlightTimelineAdminProps {
+  user: AuthUser;
+}
+
+const HighlightTimelineAdmin: React.FC<HighlightTimelineAdminProps> = () => {
+  return <>This is highlight timeline administration page.</>;
+};
+
+export default HighlightTimelineAdmin;
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index c78490d8..37ad2ef7 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -9,7 +9,6 @@ import {
 } from "react-bootstrap";
 
 import OperationDialog from "../common/OperationDialog";
-import AdminSubPage from "./AdminSubPage";
 
 import { User, AuthUser } from "@/services/user";
 import { getHttpUserClient, HttpUser } from "@/http/user";
@@ -266,7 +265,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
     });
 
     return (
-      <AdminSubPage>
+      <>
         <Row className="justify-content-end my-2">
           <Col xs="auto">
             <Button
@@ -283,7 +282,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
         </Row>
         {userComponents}
         {dialogNode}
-      </AdminSubPage>
+      </>
     );
   } else {
     return <Spinner animation="border" />;
diff --git a/FrontEnd/src/app/views/admin/admin.sass b/FrontEnd/src/app/views/admin/admin.sass
new file mode 100644
index 00000000..e69de29b
-- 
cgit v1.2.3


From 8482b649e13a9f470801d3c0a87672099da86a4e Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Fri, 20 Nov 2020 20:33:01 +0800
Subject: ...

---
 FrontEnd/src/app/locales/en/admin.json       | 16 +++++++++++++++-
 FrontEnd/src/app/locales/en/translation.json |  3 ++-
 FrontEnd/src/app/locales/zh/admin.json       | 16 +++++++++++++++-
 FrontEnd/src/app/locales/zh/translation.json |  3 ++-
 FrontEnd/src/app/views/admin/Admin.tsx       |  4 ++++
 FrontEnd/src/app/views/admin/AdminNav.tsx    |  7 +++++--
 FrontEnd/src/app/views/admin/UserAdmin.tsx   | 25 ++++++++++++++++++-------
 FrontEnd/src/app/views/common/AppBar.tsx     |  2 +-
 8 files changed, 62 insertions(+), 14 deletions(-)

(limited to 'FrontEnd/src/app/views/admin/Admin.tsx')

diff --git a/FrontEnd/src/app/locales/en/admin.json b/FrontEnd/src/app/locales/en/admin.json
index 69a88e3b..a0290206 100644
--- a/FrontEnd/src/app/locales/en/admin.json
+++ b/FrontEnd/src/app/locales/en/admin.json
@@ -1 +1,15 @@
-{}
+{
+  "nav": {
+    "users": "Users",
+    "highlightTimelines": "Highlight Timelines"
+  },
+  "create": "Create",
+  "user": {
+    "nickname": "Nickname: ",
+    "uniqueId": "Unique ID: ",
+    "permissions": "Permissions: ",
+    "modify": "Modify",
+    "modifyPermissions": "Modify Permissions",
+    "delete": "Delete"
+  }
+}
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 662a1aac..cdb6da37 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -13,7 +13,8 @@
   "nav": {
     "settings": "Settings",
     "login": "Login",
-    "about": "About"
+    "about": "About",
+    "administration": "Administration"
   },
   "chooseImage": "Choose a image",
   "loadImageError": "Failed to load image.",
diff --git a/FrontEnd/src/app/locales/zh/admin.json b/FrontEnd/src/app/locales/zh/admin.json
index 69a88e3b..d6895206 100644
--- a/FrontEnd/src/app/locales/zh/admin.json
+++ b/FrontEnd/src/app/locales/zh/admin.json
@@ -1 +1,15 @@
-{}
+{
+  "nav": {
+    "users": "用户",
+    "highlightTimelines": "高光时间线"
+  },
+  "create": "创建",
+  "user": {
+    "nickname": "昵称：",
+    "uniqueId": "唯一ID：",
+    "permissions": "权限：",
+    "modify": "修改",
+    "modifyPermissions": "修改权限",
+    "delete": "删除"
+  }
+}
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index ecd1df4b..5d28f694 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -13,7 +13,8 @@
   "nav": {
     "settings": "设置",
     "login": "登陆",
-    "about": "关于"
+    "about": "关于",
+    "administration": "管理"
   },
   "chooseImage": "选择一个图片",
   "loadImageError": "加载图片失败",
diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx
index ac640700..94e97713 100644
--- a/FrontEnd/src/app/views/admin/Admin.tsx
+++ b/FrontEnd/src/app/views/admin/Admin.tsx
@@ -4,10 +4,14 @@ import { Container } from "react-bootstrap";
 
 import { AuthUser } from "@/services/user";
 
+import i18n from "@/i18n";
+
 import AdminNav from "./AdminNav";
 import UserAdmin from "./UserAdmin";
 import HighlightTimelineAdmin from "./HighlightTimelineAdmin";
 
+void i18n.loadNamespaces("admin");
+
 interface AdminProps {
   user: AuthUser;
 }
diff --git a/FrontEnd/src/app/views/admin/AdminNav.tsx b/FrontEnd/src/app/views/admin/AdminNav.tsx
index 040b479f..f376beda 100644
--- a/FrontEnd/src/app/views/admin/AdminNav.tsx
+++ b/FrontEnd/src/app/views/admin/AdminNav.tsx
@@ -1,11 +1,14 @@
 import React from "react";
 import { Nav } from "react-bootstrap";
+import { useTranslation } from "react-i18next";
 import { useHistory, useRouteMatch } from "react-router";
 
 const AdminNav: React.FC = () => {
   const match = useRouteMatch<{ name: string }>();
   const history = useHistory();
 
+  const { t } = useTranslation();
+
   const name = match.params.name;
 
   function toggle(newTab: string): void {
@@ -21,7 +24,7 @@ const AdminNav: React.FC = () => {
             toggle("users");
           }}
         >
-          Users
+          {t("admin:nav.users")}
         </Nav.Link>
       </Nav.Item>
       <Nav.Item>
@@ -31,7 +34,7 @@ const AdminNav: React.FC = () => {
             toggle("highlighttimelines");
           }}
         >
-          Highlight Timelines
+          {t("admin:nav.highlightTimelines")}
         </Nav.Link>
       </Nav.Item>
     </Nav>
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index bd60381f..8fd9b351 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -15,6 +15,7 @@ import {
   kUserPermissionList,
   UserPermission,
 } from "@/http/user";
+import { useTranslation } from "react-i18next";
 
 interface DialogProps<TData = undefined, TReturn = undefined> {
   open: boolean;
@@ -204,6 +205,8 @@ interface UserItemProps {
 }
 
 const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
+  const { t } = useTranslation();
+
   const [editMaskVisible, setEditMaskVisible] = React.useState<boolean>(false);
 
   return (
@@ -214,10 +217,16 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
         onClick={() => setEditMaskVisible(true)}
       />
       <h4 className="text-primary">{user.username}</h4>
-      <div className="text-secondary">nickname: {user.nickname}</div>
-      <div className="text-secondary">unique id: {user.uniqueId}</div>
       <div className="text-secondary">
-        permissions:{" "}
+        {t("admin:user.nickname")}
+        {user.nickname}
+      </div>
+      <div className="text-secondary">
+        {t("admin:user.uniqueId")}
+        {user.uniqueId}
+      </div>
+      <div className="text-secondary">
+        {t("admin:user.permissions")}
         {user.permissions.map((permission) => {
           return (
             <span key={permission} className="text-danger">
@@ -231,13 +240,13 @@ const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
         onClick={() => setEditMaskVisible(false)}
       >
         <button className="text-button primary" onClick={on[kModify]}>
-          Modify
+          {t("admin:user.modify")}
         </button>
         <button className="text-button primary" onClick={on[kModifyPermission]}>
-          Modify Permission
+          {t("admin:user.modifyPermissions")}
         </button>
         <button className="text-button danger" onClick={on[kDelete]}>
-          Delete
+          {t("admin:user.delete")}
         </button>
       </div>
     </ListGroup.Item>
@@ -249,6 +258,8 @@ interface UserAdminProps {
 }
 
 const UserAdmin: React.FC<UserAdminProps> = (props) => {
+  const { t } = useTranslation();
+
   type DialogInfo =
     | null
     | {
@@ -384,7 +395,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
                 })
               }
             >
-              Create
+              {t("admin:create")}
             </Button>
           </Col>
         </Row>
diff --git a/FrontEnd/src/app/views/common/AppBar.tsx b/FrontEnd/src/app/views/common/AppBar.tsx
index 11d3de04..c862a6d3 100644
--- a/FrontEnd/src/app/views/common/AppBar.tsx
+++ b/FrontEnd/src/app/views/common/AppBar.tsx
@@ -63,7 +63,7 @@ const AppBar: React.FC = (_) => {
               activeClassName="active"
               onClick={collapse}
             >
-              Administration
+              {t("nav.administration")}
             </NavLink>
           )}
         </Nav>
-- 
cgit v1.2.3


From f1aabc06f1005b26bd1c0c5f36c98c28a62fc31e Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Sun, 22 Nov 2020 09:49:58 +0800
Subject: fix: Fix i18n.

---
 FrontEnd/src/app/views/admin/Admin.tsx | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

(limited to 'FrontEnd/src/app/views/admin/Admin.tsx')

diff --git a/FrontEnd/src/app/views/admin/Admin.tsx b/FrontEnd/src/app/views/admin/Admin.tsx
index 94e97713..446cd36d 100644
--- a/FrontEnd/src/app/views/admin/Admin.tsx
+++ b/FrontEnd/src/app/views/admin/Admin.tsx
@@ -1,22 +1,21 @@
 import React, { Fragment } from "react";
 import { Redirect, Route, Switch, useRouteMatch, match } from "react-router";
 import { Container } from "react-bootstrap";
+import { useTranslation } from "react-i18next";
 
 import { AuthUser } from "@/services/user";
 
-import i18n from "@/i18n";
-
 import AdminNav from "./AdminNav";
 import UserAdmin from "./UserAdmin";
 import HighlightTimelineAdmin from "./HighlightTimelineAdmin";
 
-void i18n.loadNamespaces("admin");
-
 interface AdminProps {
   user: AuthUser;
 }
 
 const Admin: React.FC<AdminProps> = ({ user }) => {
+  useTranslation("admin");
+
   const match = useRouteMatch();
 
   return (
-- 
cgit v1.2.3

