Commit f4da8169 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/language-translation' into 'main'

Feat/language translation

See merge request !372
parents fb18f29b 3fc11377
Pipeline #27209 passed with stages
in 7 minutes and 38 seconds
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"cookies-ts": "^1.0.5", "cookies-ts": "^1.0.5",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"google-protobuf": "^3.21.2", "google-protobuf": "^3.21.2",
"i18next": "^23.11.4",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"overlayscrollbars-react": "^0.5.1", "overlayscrollbars-react": "^0.5.1",
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-multi-backend": "^8.0.3", "react-dnd-multi-backend": "^8.0.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.1",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-use-websocket": "^4.5.0", "react-use-websocket": "^4.5.0",
"sql.js": "^1.8.0", "sql.js": "^1.8.0",
...@@ -825,9 +827,9 @@ ...@@ -825,9 +827,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.22.10", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
...@@ -4075,6 +4077,14 @@ ...@@ -4075,6 +4077,14 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
...@@ -4084,6 +4094,28 @@ ...@@ -4084,6 +4094,28 @@
"node": ">=14.18.0" "node": ">=14.18.0"
} }
}, },
"node_modules/i18next": {
"version": "23.11.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.4.tgz",
"integrity": "sha512-CCUjtd5TfaCl+mLUzAA0uPSN+AVn4fP/kWCYt/hocPUwusTpMVczdrRyOBUwk6N05iH40qiKx6q1DoNJtBIwdg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/idb-keyval": { "node_modules/idb-keyval": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
...@@ -5946,6 +5978,27 @@ ...@@ -5946,6 +5978,27 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-i18next": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz",
"integrity": "sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-intersection-observer": { "node_modules/react-intersection-observer": {
"version": "8.34.0", "version": "8.34.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz",
...@@ -7055,6 +7108,14 @@ ...@@ -7055,6 +7108,14 @@
"integrity": "sha512-4Yn+RxcCXoRLIkCY4w7TU2hZrNiQmBe0X9T3w1Do4GBiuSDHrqAa7jBavhq78+5c2yPNvHTrfmY3g70ziaH62A==", "integrity": "sha512-4Yn+RxcCXoRLIkCY4w7TU2hZrNiQmBe0X9T3w1Do4GBiuSDHrqAa7jBavhq78+5c2yPNvHTrfmY3g70ziaH62A==",
"dev": true "dev": true
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/warning": { "node_modules/warning": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
...@@ -7730,9 +7791,9 @@ ...@@ -7730,9 +7791,9 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.22.10", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"requires": { "requires": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
} }
...@@ -9906,12 +9967,28 @@ ...@@ -9906,12 +9967,28 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"requires": {
"void-elements": "3.1.0"
}
},
"human-signals": { "human-signals": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
"integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
"dev": true "dev": true
}, },
"i18next": {
"version": "23.11.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.4.tgz",
"integrity": "sha512-CCUjtd5TfaCl+mLUzAA0uPSN+AVn4fP/kWCYt/hocPUwusTpMVczdrRyOBUwk6N05iH40qiKx6q1DoNJtBIwdg==",
"requires": {
"@babel/runtime": "^7.23.2"
}
},
"idb-keyval": { "idb-keyval": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
...@@ -11185,6 +11262,15 @@ ...@@ -11185,6 +11262,15 @@
"scheduler": "^0.23.0" "scheduler": "^0.23.0"
} }
}, },
"react-i18next": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz",
"integrity": "sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==",
"requires": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
}
},
"react-intersection-observer": { "react-intersection-observer": {
"version": "8.34.0", "version": "8.34.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz",
...@@ -11925,6 +12011,11 @@ ...@@ -11925,6 +12011,11 @@
"integrity": "sha512-4Yn+RxcCXoRLIkCY4w7TU2hZrNiQmBe0X9T3w1Do4GBiuSDHrqAa7jBavhq78+5c2yPNvHTrfmY3g70ziaH62A==", "integrity": "sha512-4Yn+RxcCXoRLIkCY4w7TU2hZrNiQmBe0X9T3w1Do4GBiuSDHrqAa7jBavhq78+5c2yPNvHTrfmY3g70ziaH62A==",
"dev": true "dev": true
}, },
"void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
},
"warning": { "warning": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
......
...@@ -27,6 +27,7 @@ import ReactDOM from "react-dom/client"; ...@@ -27,6 +27,7 @@ import ReactDOM from "react-dom/client";
import { theme } from "@/ui/theme"; import { theme } from "@/ui/theme";
import { I18NProvider } from "./ui/I18N";
import { NeosRouter } from "./ui/NeosRouter"; import { NeosRouter } from "./ui/NeosRouter";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
...@@ -34,11 +35,13 @@ const root = ReactDOM.createRoot( ...@@ -34,11 +35,13 @@ const root = ReactDOM.createRoot(
); );
root.render( root.render(
<ConfigProvider theme={theme} locale={zhCN}> <I18NProvider>
<App> <ConfigProvider theme={theme} locale={zhCN}>
<ProConfigProvider dark> <App>
<NeosRouter /> <ProConfigProvider dark>
</ProConfigProvider> <NeosRouter />
</App> </ProConfigProvider>
</ConfigProvider>, </App>
</ConfigProvider>
</I18NProvider>,
); );
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
import { easings } from "@react-spring/web"; import { easings } from "@react-spring/web";
import { isMe } from "@/stores"; import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../css";
import type { AttackFunc } from "./types"; import type { AttackFunc } from "./types";
import { asyncStart } from "./utils"; import { asyncStart } from "./utils";
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { isMe } from "@/stores"; import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../css";
import type { MoveFunc } from "./types"; import type { MoveFunc } from "./types";
import { asyncStart } from "./utils"; import { asyncStart } from "./utils";
......
...@@ -2,8 +2,8 @@ import { easings } from "@react-spring/web"; ...@@ -2,8 +2,8 @@ import { easings } from "@react-spring/web";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { isMe } from "@/stores"; import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../css";
import type { MoveFunc } from "./types"; import type { MoveFunc } from "./types";
import { asyncStart } from "./utils"; import { asyncStart } from "./utils";
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { cardStore, isMe } from "@/stores"; import { cardStore, isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../css";
import type { MoveFunc } from "./types"; import type { MoveFunc } from "./types";
import { asyncStart } from "./utils"; import { asyncStart } from "./utils";
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { isMe } from "@/stores"; import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../css";
import type { MoveFunc } from "./types"; import type { MoveFunc } from "./types";
import { asyncStart } from "./utils"; import { asyncStart } from "./utils";
......
import React, { createContext, useContext, useState } from "react";
interface I18NContextType {
language: string;
changeLanguage: (newLanguage: string) => void;
}
const I18NContext = createContext<I18NContextType | undefined>(undefined);
export const I18NProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [language, setLanguage] = useState<string>("cn"); // default language
const changeLanguage = (newLanguage: string) => {
setLanguage(newLanguage);
};
return (
<I18NContext.Provider value={{ language, changeLanguage }}>
{children}
</I18NContext.Provider>
);
};
export const useI18N = (): I18NContextType => {
const context = useContext(I18NContext);
if (!context) {
throw new Error("useI18N must be used within a I18NProvider");
}
return context;
};
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Select } from "@/ui/Shared";
export const I18NSelector: React.FC = () => {
const { i18n } = useTranslation();
const onClickLanguageChange = (language: any) => {
i18n.changeLanguage(language);
};
useEffect(() => {
// Adding language state as a dependency to force re-render
// when the language changes
}, [i18n.language]);
return (
<Select
value={i18n.language}
onChange={onClickLanguageChange}
options={[
{ value: "cn", label: "简体中文" },
{ value: "en", label: "English" },
{ value: "fr", label: "Français" },
{ value: "jp", label: "日本語" },
{ value: "br", label: "português do Brasil" },
{ value: "pt", label: "português" },
{ value: "es", label: "Castellano" },
]}
/>
);
};
{
"Header": {
"HomePage": "Página Inicial",
"Match": "Match",
"DeckBuilding": "Montagem de baralho",
"PersonalCenter": "Centro pessoal",
"MyCardCommunity": "Comunidade Mengka",
"DuelDatabase": "Base de dados de duelos",
"LogOut": "Sair",
"Login": "Entrar no Mengka",
"Fullscreen": "Tela cheia"
},
"Start": {
"Title": "Plataforma de batalha online de Yu-Gi-Oh!",
"Keywords": "Código aberto, gratuito, leve",
"Details": "Neos é uma plataforma de batalha online de Yu-gi-oh de código aberto. No Neos, você pode construir decks, criar salas, convidar amigos para batalhas. Atualmente, o Neos implementou a funcionalidade de batalhar contra jogadores de plataformas como YGOpro, YGOpro2, YGOmobile e KoishiPro, e mais clientes serão suportados no futuro.",
"StartGame": "Iniciar jogo",
"LoginToGame": "Entrar no jogo"
},
"Match": {
"Deck": "Deck",
"DeckEdit": "Edição de deck",
"MCCompetitiveMatchmakingTitle": "Duelo Competitivo",
"MCCompetitiveMatchmakingDesc": "Lute ferozmente com dezenas de milhares de outros jogadores na escada do MyCard, visando ser o mais forte. Liquidação às 22:00 do último dia de cada mês, anúncio de classificação e distribuição de recompensas.",
"MCCasualMatchmakingTitle": "Duelo Casual",
"MCCasualMatchmakingDesc": "Por enquanto, deixe de lado as vitórias e as derrotas e aproveite a diversão dos duelos. Os 20 decks mais usados no combate competitivo durante a última semana serão temporariamente desativados.",
"MCCustomRoomTitle": "Sala Personalizada",
"MCCustomRoomDesc": "Crie ou entre em salas personalizadas no servidor e batalhe com amigos.",
"MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no Mengka MyCard.",
"SinglePlayerModeTitle": "Modo de um jogador",
"SinglePlayerModeDesc": "Inicie um duelo contra a IA no servidor Koishi 7210 para testar seu deck ou apenas passar o tempo.",
"CustomRoomTitle": "Sala Personalizada",
"CustomRoomDesc": "Crie uma sala com regras personalizadas e desafie seus amigos.",
"ReplayTitle": "Replay",
"ReplayDesc": "Assista livremente aos duelos passados e reviva aqueles momentos emocionantes de reversão.",
"WIPTitle": "Em desenvolvimento...",
"WIPDesc": "Aguarde por outras funcionalidades."
}
}
{
"Header": {
"HomePage": "主页",
"Match": "匹配",
"DeckBuilding": "组卡",
"PersonalCenter": "个人中心",
"MyCardCommunity": "萌卡社区",
"DuelDatabase": "决斗数据库",
"LogOut": "退出登录",
"Login": "登录萌卡",
"Fullscreen": "全屏"
},
"Start": {
"Title": "游戏王网页端对战平台",
"Keywords": "开源、免费、轻量级",
"Details": "Neos是一个开源的游戏王网页端对战平台。在Neos中,你可以组建卡组,创建房间,邀请好友进行对战。目前,Neos已经实现了与来自YGOpro、YGOpro2、YGOmobile和KoishiPro等平台的玩家进行对战的功能,而今后更多客户端也将得到支持。",
"StartGame": "开始游戏",
"LoginToGame": "登录游戏"
},
"Match": {
"Deck": "卡组",
"DeckEdit": "卡组编辑",
"MCCompetitiveMatchmakingTitle": "MC竞技匹配",
"MCCompetitiveMatchmakingDesc": "与MyCard天梯其他数万名玩家激战,力争最强。每月最后一天22点结算,公布排名并获取奖励。",
"MCCasualMatchmakingTitle": "MC娱乐匹配",
"MCCasualMatchmakingDesc": "暂且搁置胜负,享受决斗的乐趣。过去一周竞技匹配使用数最多的20个卡组将被禁用。",
"MCCustomRoomTitle": "MC服自定义房间",
"MCCustomRoomDesc": "在MC服务器上创建或者加入自定义房间,与牌友约战。",
"MCSpectatorListTitle": "MC观战列表",
"MCSpectatorListDesc": "观看萌卡MyCard上正在进行的决斗",
"SinglePlayerModeTitle": "单人模式",
"SinglePlayerModeDesc": "在Koishi 7210服务器上开启一场与AI的决斗,验证自己的卡组,或者只是打发时间。",
"CustomRoomTitle": "自定义房间",
"CustomRoomDesc": "创建自定义规则的房间,与好友约战。",
"ReplayTitle": "录像回放",
"ReplayDesc": "自由查看进行过的决斗,回味那些精彩的逆转瞬间。",
"WIPTitle": "开发中...",
"WIPDesc": "其他功能敬请期待。"
}
}
{
"Header": {
"HomePage": "Home Page",
"Match": "Match",
"DeckBuilding": "Deck Building",
"PersonalCenter": "Personal Center",
"MyCardCommunity": "Mengka Community",
"DuelDatabase": "Duel Database",
"LogOut": "Log out",
"Login": "Login to Mengka",
"Fullscreen": "Fullscreen"
},
"Start": {
"Title": "Yu-Gi-Oh! Web Based Battle Platform",
"Keywords": "Open Source, Free, Lightweight",
"Details": "Neos is an open-source Yu-gi-oh web-based battle platform. In Neos, you can build decks, create rooms, invite friends for battles. Currently, Neos has implemented the functionality to battle players from platforms such as YGOpro, YGOpro2, YGOmobile, and KoishiPro, and more clients will be supported in the future.",
"StartGame": "Start game",
"LoginToGame": "Start game"
},
"Match": {
"Deck": "Deck",
"DeckEdit": "Deck Edit",
"MCCompetitiveMatchmakingTitle": "Competitive Matchmaking",
"MCCompetitiveMatchmakingDesc": "Battle fiercely with tens of thousands of other players on MyCard ladder, aiming to be the strongest. Settlement at 22:00 on the last day of each month, ranking announcement and rewards distribution.",
"MCCasualMatchmakingTitle": "Casual Matchmaking",
"MCCasualMatchmakingDesc": "Set aside wins and losses for now, and enjoy the fun of dueling. The top 20 most used decks in competitive matchmaking over the past week will be temporarily disabled.",
"MCCustomRoomTitle": "Custom Room",
"MCCustomRoomDesc": "Create or join custom rooms on MC server and battle with friends.",
"MCSpectatorListTitle": "Spectator List",
"MCSpectatorListDesc": "Watch the duels currently taking place on Mengka MyCard.",
"SinglePlayerModeTitle": "Single Player Mode",
"SinglePlayerModeDesc": "Start a duel against AI on Koishi 7210 server to test your deck or just pass the time.",
"CustomRoomTitle": "Custom Room",
"CustomRoomDesc": "Create a room with custom rules and challenge friends.",
"ReplayTitle": "Replay",
"ReplayDesc": "Freely watch past duels and relive those exciting moments of reversal.",
"WIPTitle": "Under development...",
"WIPDesc": "Stay tuned for other features."
}
}
{
"Header": {
"HomePage": "Page d'accueil",
"Match": "Correspondance",
"DeckBuilding": "Construction de Deck",
"PersonalCenter": "Centre personnel",
"MyCardCommunity": "Communauté Mengka",
"DuelDatabase": "Base de données de duels",
"LogOut": "Déconnexion",
"Login": "Connexion à Mengka",
"Fullscreen": "Plein écran"
},
"Start": {
"Title": "Plateforme de combat Yu-gi-oh basée sur le Web",
"Keywords": "Open-source, gratuit, léger",
"Details": "Neos est une plateforme de combat Yu-gi-oh basée sur le Web open-source. Sur Neos, vous pouvez construire des decks, créer des salles, inviter des amis à se battre. Actuellement, Neos a mis en œuvre la fonctionnalité de combattre les joueurs des plateformes telles que YGOpro, YGOpro2, YGOmobile et KoishiPro, et plus de clients seront pris en charge à l'avenir.",
"StartGame": "Commencer le jeu",
"LoginToGame": "Se connecter au jeu"
},
"Match": {
"Deck": "Deck",
"DeckEdit": "Édition de deck",
"MCCompetitiveMatchmakingTitle": "Matchmaking compétitif MC",
"MCCompetitiveMatchmakingDesc": "Combattez avec ferveur contre des dizaines de milliers d'autres joueurs sur l'échelle de MyCard, visant à être le plus fort. Règlement à 22h le dernier jour de chaque mois, annonce du classement et distribution des récompenses.",
"MCCasualMatchmakingTitle": "Matchmaking détente MC",
"MCCasualMatchmakingDesc": "Mettez de côté les victoires et les défaites pour le moment, et profitez du plaisir du duel. Les 20 decks les plus utilisés en matchmaking compétitif au cours de la semaine écoulée seront temporairement désactivés.",
"MCCustomRoomTitle": "Salle personnalisée MC",
"MCCustomRoomDesc": "Créez ou rejoignez des salles personnalisées sur le serveur MC et affrontez vos amis.",
"MCSpectatorListTitle": "Liste des spectateurs",
"MCSpectatorListDesc": "Regardez les duels en cours sur Mengka MyCard.",
"SinglePlayerModeTitle": "Mode solo",
"SinglePlayerModeDesc": "Démarrez un duel contre l'IA sur le serveur Koishi 7210 pour tester votre deck ou juste passer le temps.",
"CustomRoomTitle": "Salle personnalisée",
"CustomRoomDesc": "Salle personnalisée",
"ReplayTitle": "Replay",
"ReplayDesc": "Regardez librement les duels passés et revivez ces moments passionnants de retournement.",
"WIPTitle": "En développement...",
"WIPDesc": "D'autres fonctionnalités à venir."
}
}
{
"Header": {
"HomePage": "ホームページ",
"Match": "マッチ",
"DeckBuilding": "デッキ構築",
"PersonalCenter": "個人センター",
"MyCardCommunity": "萌卡コミュニティ",
"DuelDatabase": "デュエルデータベース",
"LogOut": "ログアウト",
"Login": "萌卡にログインする",
"Fullscreen": "フルスクリーン"
},
"Start": {
"Title": "遊戯王ウェブベースのバトルプラットフォーム",
"Keywords": "オープンソース、無料、軽量",
"Details": "Neosはオープンソースの遊戯王ウェブベースのバトルプラットフォームです。Neosでは、デッキを組み、部屋を作成し、友達を招待して対戦することができます。現在、Neosでは、YGOpro、YGOpro2、YGOmobile、KoishiProなどのプラットフォームからのプレイヤーとの対戦機能が実装されており、今後はさらに多くのクライアントがサポートされる予定です。",
"StartGame": "ゲームを開始する",
"LoginToGame": "ゲームにログインする"
},
"Match": {
"Deck": "デッキ",
"DeckEdit": "デッキ編集",
"MCCompetitiveMatchmakingTitle": "MC競技マッチメイキング",
"MCCompetitiveMatchmakingDesc": "MyCardのランキングで他の何万人ものプレイヤーと激闘を繰り広げ、最強を目指します。毎月最終日の22時に決済、ランキング発表と報酬の配布が行われます。",
"MCCasualMatchmakingTitle": "MCカジュアルマッチメイキング",
"MCCasualMatchmakingDesc": "今のところ勝ち負けを置いて、デュエルの楽しみを味わってください。過去1週間の競技マッチメイキングで最も使用された20のデッキが一時的に無効にされます。",
"MCCustomRoomTitle": "MCカスタムルーム",
"MCCustomRoomDesc": "MCサーバーでカスタムルームを作成または参加し、友達と対戦しましょう。",
"MCSpectatorListTitle": "MC観戦リスト",
"MCSpectatorListDesc": "Mengka MyCardで現在進行中のデュエルを観戦しましょう。",
"SinglePlayerModeTitle": "シングルプレイヤーモード",
"SinglePlayerModeDesc": "Koishi 7210サーバーでAIとのデュエルを開始し、自分のデッキをテストしたり、ただ時間を潰したりします。",
"CustomRoomTitle": "カスタムルーム",
"CustomRoomDesc": "カスタムルールの部屋を作成し、友達と対戦しましょう。",
"ReplayTitle": "リプレイ",
"ReplayDesc": "過去のデュエルを自由に見て、それらのエキサイティングな逆転の瞬間を振り返りましょう。",
"WIPTitle": "開発中...",
"WIPDesc": "他の機能をお楽しみに。"
}
}
{
"Header": {
"HomePage": "Página Inicial",
"Match": "Match",
"DeckBuilding": "Montagem de baralho",
"PersonalCenter": "Centro pessoal",
"MyCardCommunity": "Comunidade Mengka",
"DuelDatabase": "Base de dados de duelos",
"LogOut": "Terminar sessão",
"Login": "Iniciar sessão no Mengka",
"Fullscreen": "Ecrã completo"
},
"Start": {
"Title": "Plataforma de batalha online de Yu-Gi-Oh!",
"Keywords": "Código aberto, gratuito, leve",
"Details": "Neos é uma plataforma de batalha online de Yu-gi-oh de código aberto. No Neos, você pode construir decks, criar salas, convidar amigos para batalhas. Atualmente, o Neos implementou a funcionalidade de batalhar contra jogadores de plataformas como YGOpro, YGOpro2, YGOmobile e KoishiPro, e mais clientes serão suportados no futuro.",
"StartGame": "Iniciar jogo",
"LoginToGame": "Entrar no jogo"
},
"Match": {
"Deck": "Deck",
"DeckEdit": "Edição de deck",
"MCCompetitiveMatchmakingTitle": "Duelo Competitivo",
"MCCompetitiveMatchmakingDesc": "Lute ferozmente com dezenas de milhares de outros jogadores na escada do MyCard, visando ser o mais forte. Liquidação às 22:00 do último dia de cada mês, anúncio de classificação e distribuição de recompensas.",
"MCCasualMatchmakingTitle": "Duelo Casual",
"MCCasualMatchmakingDesc": "Por enquanto, deixe de lado as vitórias e as derrotas e aproveite a diversão dos duelos. Os 20 decks mais usados no combate competitivo durante a última semana serão temporariamente desativados.",
"MCCustomRoomTitle": "Sala Personalizada",
"MCCustomRoomDesc": "Crie ou entre em salas personalizadas no servidor e batalhe com amigos.",
"MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no Mengka MyCard.",
"SinglePlayerModeTitle": "Modo de um jogador",
"SinglePlayerModeDesc": "Inicia um duelo contra a IA no servidor Koishi 7210 para testar o teu deck ou apenas passar o tempo.",
"CustomRoomTitle": "Sala Personalizada",
"CustomRoomDesc": "Crie uma sala com regras personalizadas e desafie seus amigos.",
"ReplayTitle": "Repetição",
"ReplayDesc": "Assiste livremente aos duelos passados e revive aqueles momentos emocionantes de reversão.",
"WIPTitle": "Em desenvolvimento...",
"WIPDesc": "Aguarde por outras funcionalidades."
}
}
{
"Header": {
"HomePage": " Página Principal",
"Match": "Emparejamiento",
"DeckBuilding": "Construcción de Mazo",
"PersonalCenter": "Centro personal",
"MyCardCommunity": "Comunidad Mengka",
"DuelDatabase": "Base de datos de duelos",
"LogOut": "Cerrar sesión",
"Login": "Iniciar sesión en Mengka",
"Fullscreen": "Pantalla completa"
},
"Start": {
"Title": "Plataforma de batalla basada en la web de Yu-gi-oh",
"Keywords": "Código abierto, gratuito, ligero",
"Details": "Neos es una plataforma de batalla Yu-gi-oh basada en la web de código abierto. En Neos, puedes construir mazos, crear salas, invitar a amigos a batallas. Actualmente, Neos ha implementado la funcionalidad de batallar contra jugadores de plataformas como YGOpro, YGOpro2, YGOmobile y KoishiPro, y más clientes serán compatibles en el futuro.",
"StartGame": "Comenzar juego",
"LoginToGame": "Iniciar sesión en el juego"
},
"Match": {
"Deck": "Mazo",
"DeckEdit": "Edición de mazo",
"MCCompetitiveMatchmakingTitle": "Combate competitivo en MC",
"MCCompetitiveMatchmakingDesc": "Lucha ferozmente con decenas de miles de otros jugadores en la escalera de MyCard, aspirando a ser el más fuerte. Liquidación a las 22:00 del último día de cada mes, anuncio de clasificación y distribución de recompensas.",
"MCCasualMatchmakingTitle": "Combate casual en MC",
"MCCasualMatchmakingDesc": "Por ahora, deja de lado las victorias y las derrotas, y disfruta la diversión de los duelos. Los 20 mazos más utilizados en el combate competitivo durante la última semana serán temporalmente desactivados.",
"MCCustomRoomTitle": "Sala personalizada",
"MCCustomRoomDesc": "Crea o únete a salas personalizadas en el servidor y juega con amigos.",
"MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Observa los duelos que se están llevando a cabo en Mengka MyCard.",
"SinglePlayerModeTitle": "Modo un jugador",
"SinglePlayerModeDesc": "Inicia un duelo contra la IA en el servidor Koishi 7210 para probar tu mazo o simplemente pasar el tiempo.",
"CustomRoomTitle": "Sala personalizada",
"CustomRoomDesc": "Crea una sala con reglas personalizadas y desafía a tus amigos.",
"ReplayTitle": "Repetición",
"ReplayDesc": "Mira libremente duelos pasados y revive esos emocionantes momentos de reversión.",
"WIPTitle": "En desarrollo...",
"WIPDesc": "Espere por otras funciones."
}
}
export * from "./I18NContext";
export * from "./I18NSelector";
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
/* Import all translation files */
import translationBrazilian from "./Source/Brazilian/translation.json";
import translationChinese from "./Source/Chinese/translation.json";
import translationEnglish from "./Source/English/translation.json";
import translationFrench from "./Source/French/translation.json";
import translationJapanese from "./Source/Japanese/translation.json";
import translationPortuguese from "./Source/Portuguese/translation.json";
import translationSpanish from "./Source/Spanish/translation.json";
const resources = {
cn: {
Header: translationChinese.Header,
Start: translationChinese.Start,
Match: translationChinese.Match,
},
en: {
Header: translationEnglish.Header,
Start: translationEnglish.Start,
Match: translationEnglish.Match,
},
es: {
Header: translationSpanish.Header,
Start: translationSpanish.Start,
Match: translationSpanish.Match,
},
fr: {
Header: translationFrench.Header,
Start: translationFrench.Start,
Match: translationFrench.Match,
},
jp: {
Header: translationJapanese.Header,
Start: translationJapanese.Start,
Match: translationJapanese.Match,
},
br: {
Header: translationBrazilian.Header,
Start: translationBrazilian.Start,
Match: translationBrazilian.Match,
},
pt: {
Header: translationPortuguese.Header,
Start: translationPortuguese.Start,
Match: translationPortuguese.Match,
},
};
i18next.use(initReactI18next).init({
resources,
lng: "cn", //default language
});
export default i18next;
import { Avatar, Dropdown } from "antd"; import { Avatar, Dropdown } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { import {
type LoaderFunction, type LoaderFunction,
NavLink, NavLink,
...@@ -18,6 +19,7 @@ import { ...@@ -18,6 +19,7 @@ import {
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { accountStore } from "@/stores"; import { accountStore } from "@/stores";
import { I18NSelector } from "../I18N";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { import {
getLoginStatus, getLoginStatus,
...@@ -58,6 +60,8 @@ const HeaderBtn: React.FC< ...@@ -58,6 +60,8 @@ const HeaderBtn: React.FC<
}; };
export const Component = () => { export const Component = () => {
const { t: i18n } = useTranslation("Header");
// 捕获SSO登录 // 捕获SSO登录
const routerLocation = useLocation(); const routerLocation = useLocation();
useEffect(() => { useEffect(() => {
...@@ -95,14 +99,15 @@ export const Component = () => { ...@@ -95,14 +99,15 @@ export const Component = () => {
/> />
</a> </a>
<HeaderBtn to="/">主页</HeaderBtn> <HeaderBtn to="/">{i18n("HomePage")}</HeaderBtn>
<HeaderBtn to="/match" disabled={!logined}> <HeaderBtn to="/match" disabled={!logined}>
匹配 {i18n("Match")}
</HeaderBtn> </HeaderBtn>
<HeaderBtn to="/build" disabled={!logined}> <HeaderBtn to="/build" disabled={!logined}>
组卡 {i18n("DeckBuilding")}
</HeaderBtn> </HeaderBtn>
<span style={{ flexGrow: 1 }} /> <span style={{ flexGrow: 1 }} />
<I18NSelector />
<span className={styles.profile}> <span className={styles.profile}>
<Dropdown <Dropdown
arrow arrow
...@@ -111,14 +116,14 @@ export const Component = () => { ...@@ -111,14 +116,14 @@ export const Component = () => {
{ {
label: ( label: (
<a href={NeosConfig.profileUrl} target="_blank"> <a href={NeosConfig.profileUrl} target="_blank">
个人中心 {i18n("PersonalCenter")}
</a> </a>
), ),
}, },
{ {
label: ( label: (
<a href="https://ygobbs.com" target="_blank"> <a href="https://ygobbs.com" target="_blank">
萌卡社区 {i18n("MyCardCommunity")}
</a> </a>
), ),
}, },
...@@ -128,16 +133,16 @@ export const Component = () => { ...@@ -128,16 +133,16 @@ export const Component = () => {
href="https://mycard.moe/ygopro/arena/#/" href="https://mycard.moe/ygopro/arena/#/"
target="_blank" target="_blank"
> >
决斗数据库 {i18n("DuelDatabase")}
</a> </a>
), ),
}, },
{ {
label: logined ? "退出登录" : "登录萌卡", label: logined ? i18n("LogOut") : i18n("Login"),
onClick: logined ? onLogout : onLogin, onClick: logined ? onLogout : onLogin,
}, },
{ {
label: "全屏", label: i18n("Fullscreen"),
onClick: () => document.documentElement.requestFullscreen(), onClick: () => document.documentElement.requestFullscreen(),
danger: true, danger: true,
}, },
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import { App, Button, Modal, Space } from "antd"; import { App, Button, Modal, Space } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LoaderFunction, useNavigate } from "react-router-dom"; import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
...@@ -18,7 +19,7 @@ import { ...@@ -18,7 +19,7 @@ import {
} from "@/api"; } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { accountStore, deckStore, resetUniverse, roomStore } from "@/stores"; import { accountStore, deckStore, resetUniverse, roomStore } from "@/stores";
import { Background, IconFont, Select } from "@/ui/Shared"; import { Background, IconFont, ScrollableArea, Select } from "@/ui/Shared";
import { import {
CustomRoomContent, CustomRoomContent,
...@@ -51,6 +52,7 @@ export const Component: React.FC = () => { ...@@ -51,6 +52,7 @@ export const Component: React.FC = () => {
const [entertainMatchLoading, setEntertainMatchLoading] = useState(false); // 娱乐匹配的loading状态 const [entertainMatchLoading, setEntertainMatchLoading] = useState(false); // 娱乐匹配的loading状态
const [watchLoading, setWatchLoading] = useState(false); // 观战模式的loading状态 const [watchLoading, setWatchLoading] = useState(false); // 观战模式的loading状态
const navigate = useNavigate(); const navigate = useNavigate();
const { t: i18n } = useTranslation("Match");
// 匹配 // 匹配
const onMatch = async (arena: "athletic" | "entertain") => { const onMatch = async (arena: "athletic" | "entertain") => {
...@@ -222,7 +224,7 @@ export const Component: React.FC = () => { ...@@ -222,7 +224,7 @@ export const Component: React.FC = () => {
<div className={styles.wrap}> <div className={styles.wrap}>
<Space size={16}> <Space size={16}>
<Select <Select
title="卡组" title={i18n("Deck")}
showSearch showSearch
value={deckName} value={deckName}
style={{ width: 200 }} style={{ width: 200 }}
...@@ -246,13 +248,13 @@ export const Component: React.FC = () => { ...@@ -246,13 +248,13 @@ export const Component: React.FC = () => {
onClick={() => navigate("/build")} onClick={() => navigate("/build")}
size="large" size="large"
> >
卡组编辑 {i18n("DeckEdit")}
</Button> </Button>
</Space> </Space>
<div className={styles["mode-select"]}> <div className={styles["mode-select"]}>
<Mode <Mode
title="MC竞技匹配" title={i18n("MCCompetitiveMatchmakingTitle")}
desc="与MyCard天梯其他数万名玩家激战,力争最强。每月最后一天22点结算,公布排名并获取奖励。" desc={i18n("MCCompetitiveMatchmakingDesc")}
icon={ icon={
athleticMatchLoading ? ( athleticMatchLoading ? (
<LoadingOutlined /> <LoadingOutlined />
...@@ -263,8 +265,8 @@ export const Component: React.FC = () => { ...@@ -263,8 +265,8 @@ export const Component: React.FC = () => {
onClick={onCompetitiveMatch} onClick={onCompetitiveMatch}
/> />
<Mode <Mode
title="MC娱乐匹配" title={i18n("MCCasualMatchmakingTitle")}
desc="暂且搁置胜负,享受决斗的乐趣。过去一周竞技匹配使用数最多的20个卡组将被禁用。" desc={i18n("MCCasualMatchmakingDesc")}
icon={ icon={
entertainMatchLoading ? ( entertainMatchLoading ? (
<LoadingOutlined /> <LoadingOutlined />
...@@ -275,20 +277,20 @@ export const Component: React.FC = () => { ...@@ -275,20 +277,20 @@ export const Component: React.FC = () => {
onClick={onEntertainMatch} onClick={onEntertainMatch}
/> />
<Mode <Mode
title="MC服自定义房间" title={i18n("MCCustomRoomTitle")}
desc="在MC服务器上创建或者加入自定义房间,与牌友约战。" desc={i18n("MCCustomRoomDesc")}
icon={<BulbOutlined />} icon={<BulbOutlined />}
onClick={onMCCustomRoom} onClick={onMCCustomRoom}
/> />
<Mode <Mode
title="MC观战列表" title={i18n("MCSpectatorListTitle")}
desc="观看萌卡MyCard上正在进行的决斗。" desc={i18n("MCSpectatorListDesc")}
icon={watchLoading ? <LoadingOutlined /> : <PlayCircleFilled />} icon={watchLoading ? <LoadingOutlined /> : <PlayCircleFilled />}
onClick={onMCWatch} onClick={onMCWatch}
/> />
<Mode <Mode
title="单人模式" title={i18n("SinglePlayerModeTitle")}
desc="在Koishi 7210服务器上开启一场与AI的决斗,验证自己的卡组,或者只是打发时间。" desc={i18n("SinglePlayerModeDesc")}
icon={ icon={
singleLoading ? ( singleLoading ? (
<LoadingOutlined /> <LoadingOutlined />
...@@ -299,18 +301,18 @@ export const Component: React.FC = () => { ...@@ -299,18 +301,18 @@ export const Component: React.FC = () => {
onClick={onAIMatch} onClick={onAIMatch}
/> />
<Mode <Mode
title="自定义房间" title={i18n("CustomRoomTitle")}
desc="创建自定义规则的房间,与好友约战。" desc={i18n("CustomRoomDesc")}
icon={<SettingFilled />} icon={<SettingFilled />}
onClick={onCustomRoom} onClick={onCustomRoom}
/> />
<Mode <Mode
title="录像回放" title={i18n("ReplayTitle")}
desc="自由查看进行过的决斗,回味那些精彩的逆转瞬间。" desc={i18n("ReplayDesc")}
icon={<IconFont type="icon-record" size={24} />} icon={<IconFont type="icon-record" size={24} />}
onClick={replayOpen} onClick={replayOpen}
/> />
<Mode title="开发中..." desc="其他功能敬请期待。" icon={null} /> <Mode title={i18n("WIPTitle")} desc={i18n("WIPDesc")} icon={null} />
</div> </div>
</div> </div>
</div> </div>
...@@ -328,8 +330,10 @@ const Mode: React.FC<{ ...@@ -328,8 +330,10 @@ const Mode: React.FC<{
onClick?: () => void; onClick?: () => void;
}> = ({ title, desc, icon, onClick }) => ( }> = ({ title, desc, icon, onClick }) => (
<div className={styles.mode} onClick={onClick}> <div className={styles.mode} onClick={onClick}>
<div className={styles.icon}>{icon}</div> <ScrollableArea maxHeight="15rem">
<div className={styles.title}>{title}</div> <div className={styles.icon}>{icon}</div>
<div className={styles.desc}>{desc}</div> <div className={styles.title}>{title}</div>
<div className={styles.desc}>{desc}</div>
</ScrollableArea>
</div> </div>
); );
...@@ -4,10 +4,10 @@ import classNames from "classnames"; ...@@ -4,10 +4,10 @@ import classNames from "classnames";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
export const Select: React.FC< export const Select: React.FC<
React.ComponentProps<typeof AntdSelect> & { title: string } React.ComponentProps<typeof AntdSelect> & { title?: string }
> = ({ title, className, dropdownStyle, ...rest }) => ( > = ({ title, className, dropdownStyle, ...rest }) => (
<div className={styles["custom-select"]}> <div className={styles["custom-select"]}>
<span className={styles.prefix}>{title}</span> {title && <span className={styles.prefix}>{title}</span>}
<AntdSelect <AntdSelect
className={classNames(styles.select, className)} className={classNames(styles.select, className)}
size="large" size="large"
......
...@@ -2,7 +2,6 @@ export * from "./Background"; ...@@ -2,7 +2,6 @@ export * from "./Background";
export * from "./CardEffectText"; export * from "./CardEffectText";
export * from "./Chain"; export * from "./Chain";
export * from "./chatHook"; export * from "./chatHook";
export * from "./css";
export * from "./DeckCard"; export * from "./DeckCard";
export * from "./DeckZone"; export * from "./DeckZone";
export * from "./IconFont"; export * from "./IconFont";
......
import { RightOutlined } from "@ant-design/icons"; import { RightOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
...@@ -12,6 +13,7 @@ import styles from "./index.module.scss"; ...@@ -12,6 +13,7 @@ import styles from "./index.module.scss";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { t } = useTranslation("Start");
const { user } = useSnapshot(accountStore); const { user } = useSnapshot(accountStore);
return ( return (
<> <>
...@@ -24,11 +26,9 @@ export const Component: React.FC = () => { ...@@ -24,11 +26,9 @@ export const Component: React.FC = () => {
src={`${NeosConfig.assetsPath}/neos-logo.svg`} src={`${NeosConfig.assetsPath}/neos-logo.svg`}
alt="YGO NEOS" alt="YGO NEOS"
/> />
<div className={styles.title}>游戏王网页端对战平台</div> <div className={styles.title}>{t("Title")}</div>
<div className={styles.keywords}>开源、免费、轻量级</div> <div className={styles.keywords}>{t("Keywords")}</div>
<div className={styles.details}> <div className={styles.details}>{t("Details")}</div>
Neos是一个开源的游戏王网页端对战平台。在Neos中,你可以组建卡组,创建房间,邀请好友进行对战。目前,Neos已经实现了与来自YGOpro、YGOpro2、YGOmobile和KoishiPro等平台的玩家进行对战的功能,而今后更多客户端也将得到支持。
</div>
<LoginBtn logined={Boolean(user)} /> <LoginBtn logined={Boolean(user)} />
</div> </div>
<div className={styles.right}> <div className={styles.right}>
...@@ -49,6 +49,7 @@ export const Component: React.FC = () => { ...@@ -49,6 +49,7 @@ export const Component: React.FC = () => {
Component.displayName = "Start"; Component.displayName = "Start";
const LoginBtn: React.FC<{ logined: boolean }> = ({ logined }) => { const LoginBtn: React.FC<{ logined: boolean }> = ({ logined }) => {
const { t } = useTranslation("Start");
const navigate = useNavigate(); const navigate = useNavigate();
const loginViaSSO = () => const loginViaSSO = () =>
...@@ -62,7 +63,7 @@ const LoginBtn: React.FC<{ logined: boolean }> = ({ logined }) => { ...@@ -62,7 +63,7 @@ const LoginBtn: React.FC<{ logined: boolean }> = ({ logined }) => {
style={{ marginTop: "auto" }} style={{ marginTop: "auto" }}
onClick={logined ? goToMatch : loginViaSSO} onClick={logined ? goToMatch : loginViaSSO}
> >
<span>{logined ? "开始游戏" : "登录游戏"}</span> <span>{logined ? t("StartGame") : t("LoginToGame")}</span>
<RightOutlined /> <RightOutlined />
</SpecialButton> </SpecialButton>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment