diff --git a/index.html b/index.html index 99a406e..9613834 100644 --- a/index.html +++ b/index.html @@ -15,5 +15,6 @@
+ diff --git a/package.json b/package.json index c85c6c8..921ebc8 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,13 @@ "@types/react-katex": "^3.0.4", "autoprefixer": "^10.4.16", "date-fns": "^3.3.1", + "flowbite": "^2.3.0", + "i18next": "^23.10.1", "postcss": "^8.4.33", "react": "^18.2.0", "react-dom": "^18.2.0", "react-fast-marquee": "^1.6.4", + "react-i18next": "^14.1.0", "react-katex": "^3.0.1", "react-markdown": "^9.0.1", "react-scroll-percentage": "^4.3.2", diff --git a/src/App.tsx b/src/App.tsx index 1873806..51cf766 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,10 +4,12 @@ import Mutuals from "./components/Mutuals" import Card from "./components/Card" import VerticalMenu from "./components/VerticalMenu" import HorizontalMenu from "./components/HorizontalMenu" +import LanguageMenu from "./components/LanguageMenu" import ContentRenderer from "./components/ContentRenderer" import Footer from "./components/Footer" import { ThemeProvider, ThemeSwitcher } from "./components/Theme" + import { useState, useEffect } from 'react'; export default function App() { @@ -82,6 +84,7 @@ export default function App() {
+
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 31f684c..69bbd51 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -4,11 +4,14 @@ import { RecoilRoot, useRecoilState } from 'recoil' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPaperPlane } from '@fortawesome/free-solid-svg-icons' import { formatDistance } from 'date-fns' -import { ja } from 'date-fns/locale/ja' +import { ja, enUS, zhCN } from 'date-fns/locale' import { InlineMath } from 'react-katex'; import Markdown from 'react-markdown'; +import { useTranslation } from "react-i18next"; export default function Chat() { + const { t, i18n: { language } } = useTranslation(); + const [page, _] = useRecoilState(currentPage); const [name, setName] = useState(""); const [body, setBody] = useState(""); @@ -42,19 +45,19 @@ export default function Chat() { }) } ) - .then( - (res) => res.text() - ) - .then ( - async (text) => { - if (text.includes("Success")) { - await showAlert("送信しました。"); - setBody(""); - } else { - await showAlert("送信するときになんらかの問題が発生しました。" + text); + .then( + (res) => res.text() + ) + .then( + async (text) => { + if (text.includes("Success")) { + await showAlert(t("sent")); + setBody(""); + } else { + await showAlert(t("sent_error") + text); + } } - } - ) + ) await new Promise(s => setTimeout(s, 800)); updateChat(); } @@ -63,8 +66,8 @@ export default function Chat() { fetch( "https://bbs.yude.workers.dev/api/messages", ) - .then((res) => res.json()) - .then((data) => setMessages(data)); + .then((res) => res.json()) + .then((data) => setMessages(data)); } const updateName = (e: any) => { setName(e.target.value); @@ -92,7 +95,7 @@ export default function Chat() { fill="currentColor" viewBox="0 0 20 20" > - +
{alert} @@ -101,7 +104,7 @@ export default function Chat() {
@@ -133,44 +136,43 @@ export default function Chat() { onClick={submit} type="button" aria-label="チャットを送信" - className={`w-full text-xl dark:text-white hover:text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center border border-2 border-blue-600 hover:bg-blue-700 focus:ring-blue-800${ name === "" || body === "" ? " opacity-20" : ""}`} + className={`w-full text-xl dark:text-white hover:text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center border border-2 border-blue-600 hover:bg-blue-700 focus:ring-blue-800${name === "" || body === "" ? " opacity-20" : ""}`} disabled={name === "" || body === ""} > - {" "}送信 + {" "}{t("send")}
- { - messages && - messages.map((message, index) => { - return ( -
-

- {message.name} さん,{" "} - { - formatDistance(Date.parse(message.createdAt), new Date(), { addSuffix: true, locale: ja }) - } -

-

- { - message.body.includes("$$") - ? - - {message.body.replace(/\$\$/g, "")} - - : - - {message.body} - - } -

-
- ) - }) - } + { + messages && + messages.map((message, index) => { + return ( +
+

+ {message.name},{" "} + { + formatDistance(Date.parse(message.createdAt), new Date(), { addSuffix: true, locale: (language === "ja" ? ja : language === "en" ? enUS : language === "zhCN" ? zhCN : ja) }) + } +

+

+ { + message.body.includes("$$") + ? + + {message.body.replace(/\$\$/g, "")} + + : + + {message.body} + + } +

+
+ ) + }) + }
) } - \ No newline at end of file diff --git a/src/components/Keys.tsx b/src/components/Keys.tsx index e33e4eb..6573d95 100644 --- a/src/components/Keys.tsx +++ b/src/components/Keys.tsx @@ -1,29 +1,31 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from "react-i18next"; export default function Keys() { + const { t } = useTranslation(); + return ( <> -
-

GNU Privacy Guard

-
    -
  • - GitHub  から入手できます。 -
  • -
  • - 鍵指紋: 3745 F270 DB4E 8975 6B07 62BE EB0F E5D9 25C4 A968 -
  • -
+
+

GNU Privacy Guard

+
    +
  • + GitHub  +
  • +
  • + {t("fingerprint")}: 3745 F270 DB4E 8975 6B07 62BE EB0F E5D9 25C4 A968 +
  • +
-

The Secure Shell (RFC 4716)

-
    -
  • - GitHub  から入手できます。 -
  • -
- -
+

The Secure Shell (RFC 4716)

+ + +
) - } - \ No newline at end of file +} diff --git a/src/components/LanguageMenu.tsx b/src/components/LanguageMenu.tsx new file mode 100644 index 0000000..eb09f7d --- /dev/null +++ b/src/components/LanguageMenu.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from "react-i18next"; + +export default function LanguageMenu() { + const { t, i18n: { changeLanguage, language } } = useTranslation(); + + return ( + <> + + + + + + ) +} diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 3c97872..0ac128f 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,90 +1,96 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCake, faMapPin, faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from "react-i18next"; export default function Profile() { + const { t, i18n: { language } } = useTranslation(); + + const localizedMonth = function (idx: number) { + let objDate = new Date(); + let lang = language; + objDate.setDate(1); + objDate.setMonth(idx - 1); + if (lang === "zhCN") { + lang = "zh-cn"; + } + if (language === "ja" || language == "zhCN") { + return idx; + } + return objDate.toLocaleString(lang, { month: "long" }); + } + return ( <> -
-

所属

+
+

{t("affiliation")}

-
    -
  1. -
    - -

    - - 広島市立大学 - -

    -

    - - 情報ネットワーク研究室 - -

    -

    - - センシング講座, ネットワークソフトウェア研究グループ - -

    -

    - - 情報工学科 - -

    -

    - - 情報科学部 - -

    -
  2. -
- -

- - 過去の活動  - -

+
    +
  1. +
    + +

    + + {t("hcu")} + +

    +

    + + {t("faculty")} + +

    +

    + + {t("degree")} + +

    +
  2. +
-

基本情報

- -
    -
  • -   - 2001 年 11 月 19 日 生まれ (22 歳) -
  • -
  • -   広島県 広島市 在住 -
  • -
+

+ + {t("past_activities")}  + +

-

資格, 免許

-
    -
  • - プロジェクトセカイ カラフルステージ! feat. 初音ミク フルコンボ楽曲 (MASTER) 200 曲 (2023 年 6 月) -
  • -
  • - IPA 基本情報技術者 (2022 年 4 月) -
  • -
  • - 普通自動車第一種運転免許, AT 限定 (2021 年 6 月) -
  • -
  • - TOEIC Listening & Reading: 860 点 (2022 年 12 月) -
  • -
  • - TOEIC Listening & Reading, IP テスト 960 点 (2021 年 12 月) -
  • -
  • - 実用英語技能検定 2 級 (2018 年 10 月) -
  • -
  • - 全日本弓道連盟 審査 1 級 (2015 年 6 月) -
  • -
-
+

{t("basic_info")}

+ +
    +
  • +   + {t("2001/11/19")} {t("birth", { age: "22" })} +
  • +
  • +   {t("location")} +
  • +
+ +

{t("licenses")}

+
    +
  • + プロジェクトセカイ カラフルステージ! feat. 初音ミク フルコンボ楽曲 (MASTER) 200 曲 ({t("year/month", { year: 2023, month: localizedMonth(6) })}) +
  • +
  • + IPA 基本情報技術者 ({t("year/month", { year: 2022, month: localizedMonth(4) })}) +
  • +
  • + 普通自動車第一種運転免許, AT 限定 ({t("year/month", { year: 2021, month: localizedMonth(6) })}) +
  • +
  • + TOEIC Listening & Reading: 860 点 ({t("year/month", { year: 2022, month: localizedMonth(12) })}) +
  • +
  • + TOEIC Listening & Reading, IP テスト 960 点 ({t("year/month", { year: 2021, month: localizedMonth(12) })}) +
  • +
  • + 実用英語技能検定 2 級 ({t("year/month", { year: 2018, month: localizedMonth(10) })}) +
  • +
  • + 全日本弓道連盟 審査 1 級 ({t("year/month", { year: 2015, month: localizedMonth(6) })}) +
  • +
+
) - } - \ No newline at end of file +} diff --git a/src/components/Services.tsx b/src/components/Services.tsx index eeba941..fc36be4 100644 --- a/src/components/Services.tsx +++ b/src/components/Services.tsx @@ -1,70 +1,71 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' +import { useTranslation } from "react-i18next"; export default function Services() { + const { t } = useTranslation(); + return ( <> -
-

- サービス -

- -

- 接続性 -

-

閲覧にはそれぞれのプロトコルに対応したソフトウェアが必要です。

- - -
+
+

+ {t("services")} +

+ +

+ {t("connectivity")} +

+ + +
) - } - \ No newline at end of file +} diff --git a/src/components/Theme.tsx b/src/components/Theme.tsx index 0e00f73..8dedb67 100644 --- a/src/components/Theme.tsx +++ b/src/components/Theme.tsx @@ -37,15 +37,14 @@ export const ThemeSwitcher = () => { return ( ) } diff --git a/src/components/VerticalMenu.tsx b/src/components/VerticalMenu.tsx index bae150e..7be2880 100644 --- a/src/components/VerticalMenu.tsx +++ b/src/components/VerticalMenu.tsx @@ -3,6 +3,7 @@ import { faUser, faStar, faComments } from '@fortawesome/free-regular-svg-icons' import { faLink, faKey } from '@fortawesome/free-solid-svg-icons' import { RecoilRoot, atom, useRecoilState } from 'recoil' +import { useTranslation } from "react-i18next"; export enum Pages { Profile = 0, @@ -12,7 +13,7 @@ export enum Pages { Spotify = 4, Services = 5, Chat = 6, - } +} export const currentPage = atom({ key: 'page', @@ -20,51 +21,52 @@ export const currentPage = atom({ }) export default function VerticalMenu() { + const { t } = useTranslation(); + const [page, setPage] = useRecoilState(currentPage); const activeColor = " bg-slate-700 text-white" return ( - -
-
    -
  • {setPage(Pages.Profile)}} - > - {" "} -

    プロフィール

    -
  • -
  • {setPage(Pages.Links)}} - > - {" "} -

    リンク

    -
  • -
  • {setPage(Pages.Keys)}} - > - {" "} -

    公開鍵

    -
  • -
  • {setPage(Pages.Services)}} - > - {" "} -

    サービス

    -
  • -
  • {setPage(Pages.Chat)}} - > - {" "} -

    チャット

    -
  • -
-
-
+ +
+
    +
  • { setPage(Pages.Profile) }} + > + {" "} +

    {t("profile")}

    +
  • +
  • { setPage(Pages.Links) }} + > + {" "} +

    {t("links")}

    +
  • +
  • { setPage(Pages.Keys) }} + > + {" "} +

    {t("public_keys")}

    +
  • +
  • { setPage(Pages.Services) }} + > + {" "} +

    {t("services")}

    +
  • +
  • { setPage(Pages.Chat) }} + > + {" "} +

    {t("chat")}

    +
  • +
+
+
) - } - \ No newline at end of file +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..ab34063 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,14 @@ +import i18n from "i18next"; +import { useTranslation, initReactI18next } from "react-i18next"; +import ja from './locales/ja.json' +import en from './locales/en.json' +import zhCN from './locales/zh-cn.json' + +i18n.use(initReactI18next).init({ + resources: { + en: { ...en }, + ja: { ...ja }, + zhCN: { ...zhCN }, + }, + lng: "ja", +}); diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..f255c52 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,33 @@ +{ + "translation": { + "profile": "Profile", + "links": "Links", + "public_keys": "Public keys", + "services": "Services", + "chat": "Chat", + "present": "Present", + "affiliation": "Affiliation", + "hcu": "Hiroshima City University", + "faculty": "Department of Computer and Network Engineering, Faculty of Information Sciences", + "degree": "Bachelor of Information Engineering", + "past_activities": "Past activities", + "basic_info": "Basic information", + "2001/11/19": "November 19th, 2001", + "birth": "({{age}} y/o)", + "location": "Chiba, Japan", + "licenses": "Qualifications, Licenses", + "year/month": "{{month}}, {{year}}", + "fingerprint": "Key fingerprint", + "instance": "instance", + "server": "server", + "service_status": "Service status", + "multiplayer": "multiplayer", + "connectivity": "Connectivity", + "sent": "Successfully sent.", + "message_leave": "Do you have any last words?", + "sent_error": "Error occured while sending your message: ", + "name": "Name", + "body": "Message body", + "send": "Send" + } +} diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 0000000..301906f --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,35 @@ +{ + "translation": { + "profile": "プロフィール", + "links": "リンク", + "public_keys": "公開鍵", + "services": "サービス", + "chat": "チャット", + "affiliation": "所属", + "2020/4": "2020 年 4 月", + "2024/3": "2024 年 3 月", + "present": "現在", + "hcu": "広島市立大学", + "faculty": "情報科学部 情報工学科", + "degree": "学士 (情報工学)", + "past_activities": "過去の活動", + "basic_info": "基本情報", + "2001/11/19": "2001 年 11 月 19 日", + "birth": "生まれ ({{age}} 歳)", + "location": "日本, 千葉県", + "licenses": "資格, 免許", + "year/month": "{{year}} 年 {{month}} 月", + "fingerprint": "鍵指紋", + "instance": "インスタンス", + "server": "サーバー", + "service_status": "サービス状況", + "multiplayer": "マルチプレイサーバー", + "connectivity": "接続性", + "sent": "送信しました。", + "message_leave": "言い残したいことは?", + "sent_error": "送信するときになんらかの問題が発生しました: ", + "name": "名前", + "body": "本文", + "send": "送信" + } +} diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json new file mode 100644 index 0000000..194c3ae --- /dev/null +++ b/src/locales/zh-cn.json @@ -0,0 +1,33 @@ +{ + "translation": { + "profile": "关于我", + "links": "超级链接", + "public_keys": "公钥", + "services": "服务", + "chat": "聊天", + "affiliation": "所属", + "present": "现在", + "hcu": "广岛市立大学", + "faculty": "情报科学部 信息工程系", + "degree": "学士(信息工程)", + "past_activities": "过去的活动", + "basic_info": "基本信息", + "2001/11/19": "2001 年 11 月 19 日", + "birth": "出生 ({{age}} 岁)", + "location": "日本, 千叶县", + "licenses": "资格, 执照", + "year/month": "{{year}} 年 {{month}} 月", + "fingerprint": "钥匙指纹", + "instance": "实例", + "server": "服务器", + "service_status": "服务状态", + "multiplayer": "多人服务器", + "connectivity": "连接性", + "sent": "发送.", + "message_leave": "你想留下什么?", + "sent_error": "发送时出现问题: ", + "name": "句柄名称", + "body": "文本", + "send": "发送" + } +} diff --git a/src/main.tsx b/src/main.tsx index 13c980c..4f15596 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' +import "./i18n.ts" + import './index.css' -import 'katex/dist/katex.min.css'; +import 'katex/dist/katex.min.css' ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/tailwind.config.js b/tailwind.config.js index 2a11ef4..9033a51 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,11 +4,13 @@ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", + "./node_modules/flowbite/**/*.js", ], theme: { extend: {}, }, plugins: [ require('@tailwindcss/typography'), + require('flowbite/plugin') ], }