Commit 755b5d03 authored by Chunchi Che's avatar Chunchi Che

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

Refactor/language translation

See merge request mycard/Neos!384
parents 4e7ee11c 994ce1cb
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,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-flag-kit": "^1.1.1",
"react-i18next": "^14.1.1", "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",
...@@ -2518,12 +2519,12 @@ ...@@ -2518,12 +2519,12 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -3624,9 +3625,9 @@ ...@@ -3624,9 +3625,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
...@@ -5925,6 +5926,17 @@ ...@@ -5925,6 +5926,17 @@
"react": "^18.2.0" "react": "^18.2.0"
} }
}, },
"node_modules/react-flag-kit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-flag-kit/-/react-flag-kit-1.1.1.tgz",
"integrity": "sha512-sgelSGSl0HxSL8lMM1TEBidgiGV2hOi+GaLNKeg10aBnL07vpwKvmfILlc1SzOdVyH48NElFyMj4NQn8F+/FHw==",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "14.1.1", "version": "14.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz",
...@@ -8809,12 +8821,12 @@ ...@@ -8809,12 +8821,12 @@
} }
}, },
"braces": { "braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
} }
}, },
"browserslist": { "browserslist": {
...@@ -9587,9 +9599,9 @@ ...@@ -9587,9 +9599,9 @@
} }
}, },
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
...@@ -11170,6 +11182,12 @@ ...@@ -11170,6 +11182,12 @@
"scheduler": "^0.23.0" "scheduler": "^0.23.0"
} }
}, },
"react-flag-kit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-flag-kit/-/react-flag-kit-1.1.1.tgz",
"integrity": "sha512-sgelSGSl0HxSL8lMM1TEBidgiGV2hOi+GaLNKeg10aBnL07vpwKvmfILlc1SzOdVyH48NElFyMj4NQn8F+/FHw==",
"requires": {}
},
"react-i18next": { "react-i18next": {
"version": "14.1.1", "version": "14.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz",
......
...@@ -86,9 +86,27 @@ export function getCardImgUrl(code: number, back = false) { ...@@ -86,9 +86,27 @@ export function getCardImgUrl(code: number, back = false) {
return `${ASSETS_BASE}/card_back.jpg`; return `${ASSETS_BASE}/card_back.jpg`;
} }
// Define translations for different languages (I18N)
const language = localStorage.getItem("language");
let imgUrl;
switch (language) {
case "en":
case "br":
case "pt":
case "fr":
case "es":
imgUrl = NeosConfig.releaseImgUrl.replace("zh-CN", "en-US");
break;
default:
imgUrl = NeosConfig.releaseImgUrl;
break;
}
/* End of definition (I18N) */
if (isSuperReleaseCard(code)) { if (isSuperReleaseCard(code)) {
return `${NeosConfig.preReleaseImgUrl}/${code}.jpg`; return `${NeosConfig.preReleaseImgUrl}/${code}.jpg`;
} else { } else {
return `${NeosConfig.releaseImgUrl}/${code}.jpg`; return `${imgUrl}/${code}.jpg`;
} }
} }
...@@ -2,10 +2,33 @@ import { useConfig } from "@/config"; ...@@ -2,10 +2,33 @@ import { useConfig } from "@/config";
import { fetchCard, getCardStr } from "./cards"; import { fetchCard, getCardStr } from "./cards";
const { stringsUrl } = useConfig(); let { stringsUrl } = useConfig();
export const DESCRIPTION_LIMIT = 10000; export const DESCRIPTION_LIMIT = 10000;
export async function initStrings() { export async function initStrings() {
const language = localStorage.getItem("language") || "cn";
//It currently only supports en-US, es-ES, ja-JP, ko-KR, zh-CN
switch (language) {
case "en":
case "br":
case "pt":
case "fr":
stringsUrl = stringsUrl.replace("zh-CN", "en-US");
break;
case "ja":
stringsUrl = stringsUrl.replace("zh-CN", "ja-JP");
break;
case "es":
stringsUrl = stringsUrl.replace("zh-CN", "es-ES");
break;
case "ko":
stringsUrl = stringsUrl.replace("zh-CN", "ko-KR");
break;
default:
break;
}
const strings = await (await fetch(stringsUrl)).text(); const strings = await (await fetch(stringsUrl)).text();
const lineIter = strings.split("\n"); const lineIter = strings.split("\n");
......
...@@ -62,12 +62,34 @@ interface YgoDbs { ...@@ -62,12 +62,34 @@ interface YgoDbs {
let YGODBS: YgoDbs = { release: null, preRelease: null }; let YGODBS: YgoDbs = { release: null, preRelease: null };
//It currently only supports en-US, es-ES, ja-JP, ko-KR, zh-CN
// Function to update URLs based on the language
function updateDbUrls(info: any, language: string): void {
const languageMap: { [key: string]: string } = {
en: "en-US",
br: "en-US",
pt: "en-US",
fr: "en-US",
ja: "ja-JP",
ko: "ko-KR",
es: "es-ES",
};
const locale = languageMap[language] || "zh-CN";
info.releaseDbUrl = info.releaseDbUrl.replace("zh-CN", locale);
info.preReleaseDbUrl = info.preReleaseDbUrl.replace("zh-CN", locale);
}
// FIXME: 应该有个返回值,告诉业务方本次请求的结果,比如初始化DB失败 // FIXME: 应该有个返回值,告诉业务方本次请求的结果,比如初始化DB失败
function helper<T extends sqliteCmd>(action: sqliteAction<T>) { function helper<T extends sqliteCmd>(action: sqliteAction<T>) {
switch (action.cmd) { switch (action.cmd) {
case sqliteCmd.INIT: { case sqliteCmd.INIT: {
const info = action.initInfo; const info = action.initInfo;
if (info) { if (info) {
const language = localStorage.getItem("language") || "cn";
// Update URLs based on the language
updateDbUrls(info, language);
const releasePromise = pfetch(info.releaseDbUrl, { const releasePromise = pfetch(info.releaseDbUrl, {
progressCallback: action.initInfo?.progressCallback, progressCallback: action.initInfo?.progressCallback,
}).then((res) => res.arrayBuffer()); // TODO: i18n }).then((res) => res.arrayBuffer()); // TODO: i18n
......
...@@ -15,9 +15,64 @@ const DECKERROR_EXTRACOUNT = 0x7; ...@@ -15,9 +15,64 @@ const DECKERROR_EXTRACOUNT = 0x7;
const DECKERROR_SIDECOUNT = 0x8; const DECKERROR_SIDECOUNT = 0x8;
const DECKERROR_NOTAVAIL = 0x9; const DECKERROR_NOTAVAIL = 0x9;
// Define the possible language codes (I18N)
type Language = "en" | "br" | "pt" | "fr" | "ja" | "ko" | "es" | "cn";
// Define the structure for the messages (I18N)
const messages: Record<
Language,
{ mainDeckWarning: string; extraDeckWarning: string }
> = {
en: {
mainDeckWarning: "The number of Main Deck should be 40-60 cards",
extraDeckWarning: "The number of Extra Deck should be 0-15",
},
br: {
mainDeckWarning:
"O número de cartas no Deck Principal deve ser entre 40-60",
extraDeckWarning: "O número de cartas no Deck Extra deve ser entre 0-15",
},
pt: {
mainDeckWarning:
"O número de cartas no Deck Principal deve ser entre 40-60",
extraDeckWarning: "O número de cartas no Deck Extra deve ser entre 0-15",
},
fr: {
mainDeckWarning:
"Le nombre de cartes dans le Deck Principal doit être entre 40 et 60",
extraDeckWarning:
"Le nombre de cartes dans le Deck Extra doit être entre 0 et 15",
},
ja: {
mainDeckWarning: "メインデッキの枚数は40~60枚でなければなりません",
extraDeckWarning: "エクストラデッキの枚数は0~15枚でなければなりません",
},
ko: {
mainDeckWarning: "메인 덱의 카드 수는 40-60장이어야 합니다",
extraDeckWarning: "엑스트라 덱의 카드 수는 0-15장이어야 합니다",
},
es: {
mainDeckWarning:
"El número de cartas en el Mazo Principal debe ser entre 40-60",
extraDeckWarning:
"El número de cartas en el Mazo Extra debe ser entre 0-15",
},
cn: {
mainDeckWarning: "主卡组数量应为40-60张",
extraDeckWarning: "额外卡组数量应为0-15张",
},
};
/* End of definition (I18N) */
export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) { export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) {
const { error_type, error_code } = errorMsg; const { error_type, error_code } = errorMsg;
playEffect(AudioActionType.SOUND_INFO); playEffect(AudioActionType.SOUND_INFO);
// Get the language from localStorage or default to 'cn' (I18N)
const language = (localStorage.getItem("language") || "cn") as Language;
const mainDeckWarning = messages[language].mainDeckWarning;
//const extraDeckWarning = messages[language].extraDeckWarning;
switch (error_type) { switch (error_type) {
case ErrorType.JOINERROR: { case ErrorType.JOINERROR: {
roomStore.errorMsg = fetchStrings(Region.System, 1403 + error_code); roomStore.errorMsg = fetchStrings(Region.System, 1403 + error_code);
...@@ -57,7 +112,7 @@ export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) { ...@@ -57,7 +112,7 @@ export default async function handleErrorMsg(errorMsg: ygopro.StocErrorMsg) {
break; break;
} }
case DECKERROR_MAINCOUNT: { case DECKERROR_MAINCOUNT: {
roomStore.errorMsg = "主卡组数量应为40-60张"; roomStore.errorMsg = mainDeckWarning;
break; break;
} }
case DECKERROR_EXTRACOUNT: { case DECKERROR_EXTRACOUNT: {
......
import { Button, Descriptions, type DescriptionsProps } from "antd"; import { Button, Descriptions, type DescriptionsProps } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { type CardMeta, fetchCard, fetchStrings, Region } from "@/api"; import { type CardMeta, fetchCard, fetchStrings, Region } from "@/api";
import { import {
...@@ -21,6 +22,7 @@ export const CardDetail: React.FC<{ ...@@ -21,6 +22,7 @@ export const CardDetail: React.FC<{
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
}> = ({ code, open, onClose }) => { }> = ({ code, open, onClose }) => {
const { t: i18n } = useTranslation("CardDetails");
const [card, setCard] = useState<CardMeta>(); const [card, setCard] = useState<CardMeta>();
useEffect(() => { useEffect(() => {
setCard(fetchCard(code)); setCard(fetchCard(code));
...@@ -44,20 +46,20 @@ export const CardDetail: React.FC<{ ...@@ -44,20 +46,20 @@ export const CardDetail: React.FC<{
const result: DescriptionsProps["items"] = []; const result: DescriptionsProps["items"] = [];
if (card?.data.level) { if (card?.data.level) {
result.push({ result.push({
label: "等级", label: i18n("Level"),
children: card?.data.level, children: card?.data.level,
}); });
} }
result.push({ result.push({
label: "类型", label: i18n("Type"),
children: cardType, children: cardType,
span: 2, span: 2,
}); });
if (card?.data.attribute) { if (card?.data.attribute) {
result.push({ result.push({
label: "属性", label: i18n("Attribute"),
children: fetchStrings( children: fetchStrings(
Region.System, Region.System,
Attribute2StringCodeMap.get(card?.data.attribute ?? 0) || 0, Attribute2StringCodeMap.get(card?.data.attribute ?? 0) || 0,
...@@ -67,7 +69,7 @@ export const CardDetail: React.FC<{ ...@@ -67,7 +69,7 @@ export const CardDetail: React.FC<{
if (card?.data.race) { if (card?.data.race) {
result.push({ result.push({
label: "种族", label: i18n("Race"),
children: fetchStrings( children: fetchStrings(
Region.System, Region.System,
Race2StringCodeMap.get(card?.data.race ?? 0) || 0, Race2StringCodeMap.get(card?.data.race ?? 0) || 0,
...@@ -78,20 +80,20 @@ export const CardDetail: React.FC<{ ...@@ -78,20 +80,20 @@ export const CardDetail: React.FC<{
if (isMonster(card?.data.type ?? 0)) { if (isMonster(card?.data.type ?? 0)) {
result.push({ result.push({
label: "攻击力", label: i18n("Attack"),
children: card?.data.atk, children: card?.data.atk,
}); });
if (!isLinkMonster(card?.data.type ?? 0)) { if (!isLinkMonster(card?.data.type ?? 0)) {
result.push({ result.push({
label: "守备力", label: i18n("Defence"),
children: card?.data.def, children: card?.data.def,
}); });
} }
if (card?.data.lscale) { if (card?.data.lscale) {
result.push({ result.push({
label: "灵摆刻度", label: i18n("PendulumScale"),
children: ( children: (
<> <>
{card.data.lscale} - {card.data.rscale} {card.data.lscale} - {card.data.rscale}
...@@ -125,7 +127,11 @@ export const CardDetail: React.FC<{ ...@@ -125,7 +127,11 @@ export const CardDetail: React.FC<{
size="small" size="small"
items={desc.filter(Boolean).map((d, i) => ({ items={desc.filter(Boolean).map((d, i) => ({
label: label:
desc.length > 1 ? (i ? "怪兽效果" : "灵摆效果") : "卡片效果", desc.length > 1
? i
? i18n("MonsterEffect")
: i18n("PendulumEffect")
: i18n("CardEffect"),
span: 3, span: 3,
children: <CardEffectText desc={d} />, children: <CardEffectText desc={d} />,
}))} }))}
......
...@@ -2,6 +2,7 @@ import { App, Dropdown, message, Pagination } from "antd"; ...@@ -2,6 +2,7 @@ import { App, Dropdown, message, Pagination } from "antd";
import { MessageInstance } from "antd/es/message/interface"; import { MessageInstance } from "antd/es/message/interface";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import React, { memo, useEffect } from "react"; import React, { memo, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { type INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio"; import { type INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import YGOProDeck from "ygopro-deck-encode"; import YGOProDeck from "ygopro-deck-encode";
...@@ -41,7 +42,7 @@ const store = proxy<Props>({ ...@@ -41,7 +42,7 @@ const store = proxy<Props>({
export const DeckResults: React.FC = memo(() => { export const DeckResults: React.FC = memo(() => {
const snap = useSnapshot(store); const snap = useSnapshot(store);
const { message } = App.useApp(); const { message } = App.useApp();
const { t: i18n } = useTranslation("DeckResults");
useEffect(() => { useEffect(() => {
if (snap.onlyMine) { if (snap.onlyMine) {
// show only decks uploaded by myself // show only decks uploaded by myself
...@@ -116,7 +117,7 @@ export const DeckResults: React.FC = memo(() => { ...@@ -116,7 +117,7 @@ export const DeckResults: React.FC = memo(() => {
) : ( ) : (
<div className={styles.empty}> <div className={styles.empty}>
<IconFont type="icon-empty" size={40} /> <IconFont type="icon-empty" size={40} />
<div>找不到相应卡组</div> <div>{i18n("NoDeckGroupFound")}</div>
</div> </div>
)} )}
</> </>
......
...@@ -11,6 +11,7 @@ import { isEqual } from "lodash-es"; ...@@ -11,6 +11,7 @@ import { isEqual } from "lodash-es";
import { OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useDrop } from "react-dnd"; import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { CardMeta, searchCards } from "@/api"; import { CardMeta, searchCards } from "@/api";
import { isToken } from "@/common"; import { isToken } from "@/common";
...@@ -51,19 +52,19 @@ export const DeckDatabase: React.FC = () => { ...@@ -51,19 +52,19 @@ export const DeckDatabase: React.FC = () => {
((a.data?.[key] ?? 0) - (b.data?.[key] ?? 0)) * scale, ((a.data?.[key] ?? 0) - (b.data?.[key] ?? 0)) * scale,
); );
}; };
const { t } = useTranslation("BuildDeck");
const dropdownOptions: MenuProps["items"] = ( const dropdownOptions: MenuProps["items"] = (
[ [
["从新到旧", () => setSortRef((a, b) => b.id - a.id)], [`${t("FromNewToOld")}`, () => setSortRef((a, b) => b.id - a.id)],
["从旧到新", () => setSortRef((a, b) => a.id - b.id)], [`${t("FromOldToNew")}`, () => setSortRef((a, b) => a.id - b.id)],
["攻击力从高到低", genSort("atk", -1)], [`${t("AttackPowerFromHighToLow")}`, genSort("atk", -1)],
["攻击力从低到高", genSort("atk")], [`${t("AttackPowerFromLowToHigh")}`, genSort("atk")],
["守备力从高到低", genSort("def", -1)], [`${t("DefensePowerFromHighToLow")}`, genSort("def", -1)],
["守备力从低到高", genSort("def")], [`${t("DefensePowerFromLowToHigh")}`, genSort("def")],
["星/阶/刻/Link从高到低", genSort("level", -1)], [`${t("StarsRanksLevelsLinkFromHighToLow")}`, genSort("level", -1)],
["星/阶/刻/Link从低到高", genSort("level")], [`${t("StarsRanksLevelsLinkFromLowToHigh")}`, genSort("level")],
["灵摆刻度从高到低", genSort("lscale", -1)], [`${t("PendulumScaleFromHighToLow")}`, genSort("lscale", -1)],
["灵摆刻度从低到高", genSort("lscale")], [`${t("PendulumScaleFromLowToHigh")}`, genSort("lscale")],
] as const ] as const
).map(([label, onClick], key) => ({ key, label, onClick })); ).map(([label, onClick], key) => ({ key, label, onClick }));
...@@ -120,12 +121,12 @@ export const DeckDatabase: React.FC = () => { ...@@ -120,12 +121,12 @@ export const DeckDatabase: React.FC = () => {
const viewport = ref.current?.osInstance()?.elements().viewport; const viewport = ref.current?.osInstance()?.elements().viewport;
if (viewport) viewport.scrollTop = 0; if (viewport) viewport.scrollTop = 0;
}, []); }, []);
const { t: i18n } = useTranslation("BuildDeck");
return ( return (
<div className={styles.container} ref={dropRef}> <div className={styles.container} ref={dropRef}>
<Space className={styles.title} direction="horizontal"> <Space className={styles.title} direction="horizontal">
<Input <Input
placeholder="关键词(空格分隔)" placeholder={i18n("KeywordsPlaceholder")}
bordered={false} bordered={false}
suffix={ suffix={
<Button <Button
...@@ -145,7 +146,7 @@ export const DeckDatabase: React.FC = () => { ...@@ -145,7 +146,7 @@ export const DeckDatabase: React.FC = () => {
icon={<SwapOutlined />} icon={<SwapOutlined />}
onClick={() => setShowMdproDecks(!showMdproDecks)} onClick={() => setShowMdproDecks(!showMdproDecks)}
> >
{showMdproDecks ? "卡片数据库" : "Mdpro在线卡组"} {showMdproDecks ? i18n("CardDatabase") : i18n("MDProOnlineDeck")}
</Button> </Button>
</Space> </Space>
<div className={styles["select-btns"]}> <div className={styles["select-btns"]}>
...@@ -155,8 +156,8 @@ export const DeckDatabase: React.FC = () => { ...@@ -155,8 +156,8 @@ export const DeckDatabase: React.FC = () => {
style={{ width: "18.90rem" }} style={{ width: "18.90rem" }}
defaultValue={false} defaultValue={false}
options={[ options={[
{ value: true, label: "只显示我上传的卡组" }, { value: true, label: i18n("OnlyShowDecksIUploaded") },
{ value: false, label: "显示全部在线卡组" }, { value: false, label: i18n("ShowAllOnlineDecks") },
]} ]}
onChange={ onChange={
// @ts-ignore // @ts-ignore
...@@ -175,7 +176,7 @@ export const DeckDatabase: React.FC = () => { ...@@ -175,7 +176,7 @@ export const DeckDatabase: React.FC = () => {
icon={<FilterOutlined />} icon={<FilterOutlined />}
onClick={showFilterModal} onClick={showFilterModal}
> >
筛选 {i18n("Filter")}
</Button> </Button>
)} )}
<Dropdown <Dropdown
...@@ -191,7 +192,7 @@ export const DeckDatabase: React.FC = () => { ...@@ -191,7 +192,7 @@ export const DeckDatabase: React.FC = () => {
icon={<SortAscendingOutlined />} icon={<SortAscendingOutlined />}
> >
<span> <span>
排列 {i18n("SortBy")}
<span className={styles["search-count"]}> <span className={styles["search-count"]}>
({searchCardResult.length}) ({searchCardResult.length})
</span> </span>
...@@ -210,7 +211,7 @@ export const DeckDatabase: React.FC = () => { ...@@ -210,7 +211,7 @@ export const DeckDatabase: React.FC = () => {
handleSearch(emptySearchConditions); handleSearch(emptySearchConditions);
}} }}
> >
重置 {i18n("Reset")}
</Button> </Button>
</div> </div>
<ScrollableArea className={styles["search-cards-container"]} ref={ref}> <ScrollableArea className={styles["search-cards-container"]} ref={ref}>
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import { App, Button, Dropdown, MenuProps, UploadProps } from "antd"; import { App, Button, Dropdown, MenuProps, UploadProps } from "antd";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import YGOProDeck from "ygopro-deck-encode"; import YGOProDeck from "ygopro-deck-encode";
import { uploadDeck } from "@/api"; import { uploadDeck } from "@/api";
...@@ -29,6 +30,7 @@ export const DeckSelect: React.FC<{ ...@@ -29,6 +30,7 @@ export const DeckSelect: React.FC<{
}> = ({ decks, selected, onSelect, onDelete, onDownload, onCopy }) => { }> = ({ decks, selected, onSelect, onDelete, onDownload, onCopy }) => {
const newDeck = useRef<IDeck[]>([]); const newDeck = useRef<IDeck[]>([]);
const { modal, message } = App.useApp(); const { modal, message } = App.useApp();
const { t: i18n } = useTranslation("DeckSelect");
/** 创建卡组,直接给一个命名,用户可以手动修改,无需modal打断流程 */ /** 创建卡组,直接给一个命名,用户可以手动修改,无需modal打断流程 */
const createNewDeck = async () => { const createNewDeck = async () => {
...@@ -109,17 +111,17 @@ export const DeckSelect: React.FC<{ ...@@ -109,17 +111,17 @@ export const DeckSelect: React.FC<{
const items: MenuProps["items"] = [ const items: MenuProps["items"] = [
{ {
label: "新建卡组", label: `${i18n("CreateNewDeck")}`,
icon: <PlusOutlined />, icon: <PlusOutlined />,
onClick: createNewDeck, onClick: createNewDeck,
}, },
{ {
label: "从本地文件导入", label: `${i18n("ImportFromLocalFile")}`,
icon: <FileAddOutlined />, icon: <FileAddOutlined />,
onClick: showUploadModal, onClick: showUploadModal,
}, },
{ {
label: "从剪贴板导入", label: `${i18n("ImportFromClipboard")}`,
icon: <CopyOutlined />, icon: <CopyOutlined />,
onClick: importFromClipboard, onClick: importFromClipboard,
}, },
...@@ -173,8 +175,8 @@ export const DeckSelect: React.FC<{ ...@@ -173,8 +175,8 @@ export const DeckSelect: React.FC<{
onClick={cancelBubble(async () => { onClick={cancelBubble(async () => {
const result = await onCopy(deck.deckName); const result = await onCopy(deck.deckName);
result result
? message.success("复制成功") ? message.success(`${i18n("CopySuccessful")}`)
: message.error("复制失败"); : message.error(`${i18n("CopyFailed")}`);
})} })}
/> />
<Button <Button
...@@ -221,6 +223,7 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({ ...@@ -221,6 +223,7 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({
}) => { }) => {
const [uploadState, setUploadState] = useState(""); const [uploadState, setUploadState] = useState("");
const { message } = App.useApp(); const { message } = App.useApp();
const { t: i18n } = useTranslation("DeckSelect");
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
name: "file", name: "file",
multiple: true, multiple: true,
...@@ -252,8 +255,8 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({ ...@@ -252,8 +255,8 @@ const DeckUploader: React.FC<{ onLoaded: (deck: IDeck) => void }> = ({
return ( return (
<Uploader <Uploader
{...uploadProps} {...uploadProps}
text="单击或拖动文件到此区域进行上传" text={i18n("ClickOrDragFilesHereToUpload")}
hint="仅支持后缀名为ydk的卡组文件。" hint={i18n("SupportsYdkExtension")}
/> />
); );
}; };
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
Tooltip, Tooltip,
} from "antd"; } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { fetchStrings, Region } from "@/api"; import { fetchStrings, Region } from "@/api";
import { import {
...@@ -47,12 +48,15 @@ export const Filter: React.FC<{ ...@@ -47,12 +48,15 @@ export const Filter: React.FC<{
value: key, value: key,
label: fetchStrings(Region.System, value), label: fetchStrings(Region.System, value),
})); }));
const { t: i18n } = useTranslation("Filter");
const T = [ const T = [
[genOptions(Attribute2StringCodeMap), "属性", "attributes"], [genOptions(Attribute2StringCodeMap), i18n("Attribute"), "attributes"],
[genOptions(Race2StringCodeMap), "种族", "races"], [genOptions(Race2StringCodeMap), i18n("Race"), "races"],
[genOptions(Type2StringCodeMap), "类型", "types"], [genOptions(Type2StringCodeMap), i18n("Type"), "types"],
[levels, "星级", "levels"], [levels, i18n("Level"), "levels"],
[lscales, "灵摆刻度", "lscales"], [lscales, i18n("PendulumScale"), "lscales"],
] as const; ] as const;
const handleInputNumberChange = const handleInputNumberChange =
...@@ -68,7 +72,7 @@ export const Filter: React.FC<{ ...@@ -68,7 +72,7 @@ export const Filter: React.FC<{
return ( return (
<> <>
<div className={styles.title}>卡片筛选</div> <div className={styles.title}>{i18n("CardFilter")}</div>
<div className={styles.form}> <div className={styles.form}>
{T.map(([options, title, key]) => ( {T.map(([options, title, key]) => (
<Item title={title} key={key}> <Item title={title} key={key}>
...@@ -79,31 +83,31 @@ export const Filter: React.FC<{ ...@@ -79,31 +83,31 @@ export const Filter: React.FC<{
/> />
</Item> </Item>
))} ))}
<Item title="攻击力" showTip> <Item title={i18n("Attack")} showTip>
<div className={styles.number}> <div className={styles.number}>
<CustomInputNumber <CustomInputNumber
placeholder="最小值" placeholder={i18n("Minimum")}
onChange={handleInputNumberChange("atk", "min")} onChange={handleInputNumberChange("atk", "min")}
value={newConditions.atk.min} value={newConditions.atk.min}
/> />
<span className={styles.divider}>~</span> <span className={styles.divider}>~</span>
<CustomInputNumber <CustomInputNumber
placeholder="最大值" placeholder={i18n("Maximum")}
onChange={handleInputNumberChange("atk", "max")} onChange={handleInputNumberChange("atk", "max")}
value={newConditions.atk.max} value={newConditions.atk.max}
/> />
</div> </div>
</Item> </Item>
<Item title="守备力" showTip> <Item title={i18n("Defense")} showTip>
<div className={styles.number}> <div className={styles.number}>
<CustomInputNumber <CustomInputNumber
placeholder="最小值" placeholder={i18n("Minimum")}
onChange={handleInputNumberChange("def", "min")} onChange={handleInputNumberChange("def", "min")}
value={newConditions.def.min} value={newConditions.def.min}
/> />
<span className={styles.divider}>~</span> <span className={styles.divider}>~</span>
<CustomInputNumber <CustomInputNumber
placeholder="最大值" placeholder={i18n("Maximum")}
onChange={handleInputNumberChange("def", "max")} onChange={handleInputNumberChange("def", "max")}
value={newConditions.def.max} value={newConditions.def.max}
/> />
...@@ -117,10 +121,10 @@ export const Filter: React.FC<{ ...@@ -117,10 +121,10 @@ export const Filter: React.FC<{
onConfirm(newConditions); onConfirm(newConditions);
}} }}
> >
确定 {i18n("Confirm")}
</Button> </Button>
<Button type="text" onClick={onCancel}> <Button type="text" onClick={onCancel}>
&nbsp; {i18n("Cancel")}
</Button> </Button>
</div> </div>
</> </>
...@@ -151,12 +155,13 @@ const CustomSelect: React.FC<{ ...@@ -151,12 +155,13 @@ const CustomSelect: React.FC<{
defaultValue: number[]; defaultValue: number[];
onChange: (values: number[]) => void; onChange: (values: number[]) => void;
}> = ({ options, defaultValue, onChange }) => { }> = ({ options, defaultValue, onChange }) => {
const { t: i18n } = useTranslation("Filter");
return ( return (
<Select <Select
mode="multiple" mode="multiple"
allowClear allowClear
style={{ width: "100%" }} style={{ width: "100%" }}
placeholder="请选择" placeholder={i18n("Select")}
options={options} options={options}
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={onChange} onChange={onChange}
......
...@@ -11,6 +11,7 @@ import { App, Button, Input, message, Space, Tooltip } from "antd"; ...@@ -11,6 +11,7 @@ import { App, Button, Input, message, Space, Tooltip } from "antd";
import { HTML5toTouch } from "rdndmb-html5-to-touch"; import { HTML5toTouch } from "rdndmb-html5-to-touch";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DndProvider } from "react-dnd-multi-backend"; import { DndProvider } from "react-dnd-multi-backend";
import { useTranslation } from "react-i18next";
import { LoaderFunction } from "react-router-dom"; import { LoaderFunction } from "react-router-dom";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils"; import { subscribeKey } from "valtio/utils";
...@@ -87,10 +88,10 @@ export const Component: React.FC = () => { ...@@ -87,10 +88,10 @@ export const Component: React.FC = () => {
const { deck: snapSelectedDeck } = useSnapshot(selectedDeck); const { deck: snapSelectedDeck } = useSnapshot(selectedDeck);
const { message } = App.useApp(); const { message } = App.useApp();
const { t: i18n } = useTranslation("BuildDeck");
const handleDeckEditorReset = async () => { const handleDeckEditorReset = async () => {
editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck)); editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck));
message.info("重置成功"); message.info(`${i18n("ResetSuccessful")}`);
}; };
const handleDeckEditorSave = async () => { const handleDeckEditorSave = async () => {
...@@ -98,7 +99,7 @@ export const Component: React.FC = () => { ...@@ -98,7 +99,7 @@ export const Component: React.FC = () => {
const result = await deckStore.update(selectedDeck.deck.deckName, tmpIDeck); const result = await deckStore.update(selectedDeck.deck.deckName, tmpIDeck);
if (result) { if (result) {
setSelectedDeck(tmpIDeck); setSelectedDeck(tmpIDeck);
message.info("保存成功"); message.info(`${i18n("SaveSuccessful")}`);
editDeckStore.edited = false; editDeckStore.edited = false;
} else { } else {
editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck)); editDeckStore.set(await iDeckToEditingDeck(selectedDeck.deck as IDeck));
...@@ -235,12 +236,12 @@ export const DeckEditor: React.FC<{ ...@@ -235,12 +236,12 @@ export const DeckEditor: React.FC<{
} }
event.preventDefault(); event.preventDefault();
}; };
const { t: i18n } = useTranslation("BuildDeck");
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Space className={styles.title}> <Space className={styles.title}>
<Input <Input
placeholder="请输入卡组名字" placeholder={i18n("EnterTheDeckName")}
bordered={false} bordered={false}
prefix={<EditOutlined />} prefix={<EditOutlined />}
style={{ width: "8.8rem" }} style={{ width: "8.8rem" }}
...@@ -254,7 +255,7 @@ export const DeckEditor: React.FC<{ ...@@ -254,7 +255,7 @@ export const DeckEditor: React.FC<{
icon={<SwapOutlined />} icon={<SwapOutlined />}
onClick={onShuffle} onClick={onShuffle}
> >
打乱 {i18n("Shuffle")}
</Button> </Button>
<Button <Button
type="text" type="text"
...@@ -262,7 +263,7 @@ export const DeckEditor: React.FC<{ ...@@ -262,7 +263,7 @@ export const DeckEditor: React.FC<{
icon={<RetweetOutlined />} icon={<RetweetOutlined />}
onClick={onSort} onClick={onSort}
> >
排序 {i18n("Sort")}
</Button> </Button>
<Button <Button
type="text" type="text"
...@@ -270,7 +271,7 @@ export const DeckEditor: React.FC<{ ...@@ -270,7 +271,7 @@ export const DeckEditor: React.FC<{
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={onClear} onClick={onClear}
> >
清空 {i18n("Clear")}
</Button> </Button>
<Button <Button
type="text" type="text"
...@@ -278,7 +279,7 @@ export const DeckEditor: React.FC<{ ...@@ -278,7 +279,7 @@ export const DeckEditor: React.FC<{
icon={<UndoOutlined />} icon={<UndoOutlined />}
onClick={() => onReset()} onClick={() => onReset()}
> >
重置 {i18n("Reset")}
</Button> </Button>
<Button <Button
type={snapEditDeck.edited ? "primary" : "text"} type={snapEditDeck.edited ? "primary" : "text"}
...@@ -286,9 +287,9 @@ export const DeckEditor: React.FC<{ ...@@ -286,9 +287,9 @@ export const DeckEditor: React.FC<{
icon={<CheckOutlined />} icon={<CheckOutlined />}
onClick={() => onSave()} onClick={() => onSave()}
> >
保存 {i18n("Save")}
</Button> </Button>
<Tooltip title="双击添加卡片,单击右键删除卡片,按下滑轮在主卡组和副卡组之间切换卡片"> <Tooltip title={i18n("QuestionCircleTooltip")}>
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</Tooltip> </Tooltip>
</Space> </Space>
......
...@@ -7,6 +7,89 @@ import { Type } from "@/ui/Shared/DeckZone"; ...@@ -7,6 +7,89 @@ import { Type } from "@/ui/Shared/DeckZone";
import { compareCards, type EditingDeck } from "./utils"; import { compareCards, type EditingDeck } from "./utils";
// Define the possible language codes (I18N)
type Language = "en" | "br" | "pt" | "fr" | "ja" | "ko" | "es" | "cn";
// Define the structure for the messages (I18N)
const messages: Record<
Language,
{
cardTypeNotMatch: string;
exceedsNumberCardsSameName: string;
limitCards: string;
exceedsLimit: string;
cannotAddTokens: string;
}
> = {
en: {
cardTypeNotMatch: "The Card Type does not match",
exceedsNumberCardsSameName: "The number of Extra Deck should be 0-15",
limitCards: "Limit of cards",
exceedsLimit: "Exceeds the limit",
cannotAddTokens: "Cannot add tokens",
},
br: {
cardTypeNotMatch: "O Tipo de Carta não corresponde",
exceedsNumberCardsSameName: "Excede o número de cartas com o mesmo nome",
limitCards: "Limite de cartas",
exceedsLimit: "Excede o limite",
cannotAddTokens: "Não é possível adicionar fichas",
},
pt: {
cardTypeNotMatch: "O Tipo de Carta não corresponde",
exceedsNumberCardsSameName: "Excede o número de cartas com o mesmo nome",
limitCards: "Limite de cartas",
exceedsLimit: "Excede o limite",
cannotAddTokens: "Não é possível adicionar fichas",
},
fr: {
cardTypeNotMatch: "Le Type de Carte ne correspond pas",
exceedsNumberCardsSameName: "Dépasse le nombre de cartes avec le même nom",
limitCards: "Limite de cartes",
exceedsLimit: "Dépasse la limite",
cannotAddTokens: "Impossible d'ajouter des jetons",
},
ja: {
cardTypeNotMatch: "カードタイプが一致しません",
exceedsNumberCardsSameName: "同名カードの枚数を超えています",
limitCards: "カードの制限",
exceedsLimit: "制限を超えています",
cannotAddTokens: "トークンを追加できません",
},
ko: {
cardTypeNotMatch: "카드 유형이 일치하지 않습니다",
exceedsNumberCardsSameName: "동일한 이름의 카드 수를 초과합니다",
limitCards: "카드 제한",
exceedsLimit: "제한을 초과합니다",
cannotAddTokens: "토큰을 추가할 수 없습니다",
},
es: {
cardTypeNotMatch: "El Tipo de Carta no coincide",
exceedsNumberCardsSameName:
"Supera el número de cartas con el mismo nombre",
limitCards: "Límite de cartas",
exceedsLimit: "Supera el límite",
cannotAddTokens: "No se pueden agregar fichas",
},
cn: {
cardTypeNotMatch: "卡片种类不符合",
exceedsNumberCardsSameName: "超过同名卡",
limitCards: "张的上限",
exceedsLimit: "超过",
cannotAddTokens: "不能添加衍生物",
},
};
// Get the language from localStorage or default to 'cn' (I18N)
const language = (localStorage.getItem("language") || "cn") as Language;
const cardTypeNotMatch = messages[language].cardTypeNotMatch;
const exceedsNumberCardsSameName =
messages[language].exceedsNumberCardsSameName;
const limitCards = messages[language].limitCards;
const exceedsLimit = messages[language].exceedsLimit;
const cannotAddTokens = messages[language].cannotAddTokens;
/* End of definition (I18N) */
export const editDeckStore = proxy({ export const editDeckStore = proxy({
deckName: "", deckName: "",
main: [] as CardMeta[], main: [] as CardMeta[],
...@@ -75,13 +158,13 @@ export const editDeckStore = proxy({ ...@@ -75,13 +158,13 @@ export const editDeckStore = proxy({
if (isToken(cardType)) { if (isToken(cardType)) {
result = false; result = false;
reason = "不能添加衍生物"; reason = cannotAddTokens;
} }
const countLimit = type === "main" ? 60 : 15; const countLimit = type === "main" ? 60 : 15;
if (deckType.length >= countLimit) { if (deckType.length >= countLimit) {
result = false; result = false;
reason = `超过 ${countLimit} 张的上限`; reason = `${exceedsLimit} ${countLimit} ${limitCards}`;
} }
if ( if (
...@@ -89,9 +172,8 @@ export const editDeckStore = proxy({ ...@@ -89,9 +172,8 @@ export const editDeckStore = proxy({
(type === "main" && isExtraDeckCard(cardType)) (type === "main" && isExtraDeckCard(cardType))
) { ) {
result = false; result = false;
reason = "卡片种类不符合"; reason = cardTypeNotMatch;
} }
const max = 3; // 这里无需参考禁卡表 const max = 3; // 这里无需参考禁卡表
const numOfSameCards = const numOfSameCards =
editDeckStore editDeckStore
...@@ -105,7 +187,7 @@ export const editDeckStore = proxy({ ...@@ -105,7 +187,7 @@ export const editDeckStore = proxy({
if (numOfSameCards >= max) { if (numOfSameCards >= max) {
result = false; result = false;
reason = `超过同名卡 ${max} 张的上限`; reason = `${exceedsNumberCardsSameName} ${max} ${limitCards}`;
} }
return { result, reason }; return { result, reason };
......
...@@ -16,6 +16,80 @@ const defaultProps = { isOpen: false, positions: [] }; ...@@ -16,6 +16,80 @@ const defaultProps = { isOpen: false, positions: [] };
const localStore = proxy<PositionModalProps>(defaultProps); const localStore = proxy<PositionModalProps>(defaultProps);
// Define a type for translations with an index signature (I18N)
interface Translations {
[key: string]: {
Title: string;
FACEUP_ATTACK: string;
FACEUP_DEFENSE: string;
FACEDOWN_ATTACK: string;
FACEDOWN_DEFENSE: string;
};
}
// Retrieve language from localStorage or default to "cn"
const language = localStorage.getItem("language") || "cn";
// Define translations for different languages (I18N)
const translations: Translations = {
en: {
Title: "Please select a position",
FACEUP_ATTACK: "Face-Up Attack",
FACEUP_DEFENSE: "Face-Up Defense",
FACEDOWN_ATTACK: "Face-Down Attack",
FACEDOWN_DEFENSE: "Face-Down Defense",
},
br: {
Title: "Por favor, selecione uma posição",
FACEUP_ATTACK: "Ataque com a Face para Cima",
FACEUP_DEFENSE: "Defesa com a Face para Cima",
FACEDOWN_ATTACK: "Ataque com a Face para Baixo",
FACEDOWN_DEFENSE: "Defesa com a Face para Baixo",
},
pt: {
Title: "Por favor, selecione uma posição",
FACEUP_ATTACK: "Ataque com a Face para Cima",
FACEUP_DEFENSE: "Defesa com a Face para Cima",
FACEDOWN_ATTACK: "Ataque com a Face para Baixo",
FACEDOWN_DEFENSE: "Defesa com a Face para Baixo",
},
fr: {
Title: "Veuillez sélectionner une position",
FACEUP_ATTACK: "Attaque Face Visible",
FACEUP_DEFENSE: "Défense Face Visible",
FACEDOWN_ATTACK: "Attaque Face Cachée",
FACEDOWN_DEFENSE: "Défense Face Cachée",
},
ja: {
Title: "ポジションを選択してください",
FACEUP_ATTACK: "表側攻撃表示",
FACEUP_DEFENSE: "表側守備表示",
FACEDOWN_ATTACK: "裏側攻撃表示",
FACEDOWN_DEFENSE: "裏側守備表示",
},
ko: {
Title: "포지션을 선택해주세요",
FACEUP_ATTACK: "앞면 공격 표시",
FACEUP_DEFENSE: "앞면 수비 표시",
FACEDOWN_ATTACK: "뒷면 공격 표시",
FACEDOWN_DEFENSE: "뒷면 수비 표시",
},
es: {
Title: "Por favor, seleccione una posición",
FACEUP_ATTACK: "Ataque en Posición de Ataque",
FACEUP_DEFENSE: "Defensa en Posición de Ataque",
FACEDOWN_ATTACK: "Ataque en Posición de Defensa",
FACEDOWN_DEFENSE: "Defensa en Posición de Defensa",
},
cn: {
Title: "请选择表示形式",
FACEUP_ATTACK: "正面攻击形式",
FACEUP_DEFENSE: "正面防守形式",
FACEDOWN_ATTACK: "背面攻击形式",
FACEDOWN_DEFENSE: "背面防守形式",
},
};
export const PositionModal = () => { export const PositionModal = () => {
const { isOpen, positions } = useSnapshot(localStore); const { isOpen, positions } = useSnapshot(localStore);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>( const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
...@@ -24,7 +98,7 @@ export const PositionModal = () => { ...@@ -24,7 +98,7 @@ export const PositionModal = () => {
return ( return (
<NeosModal <NeosModal
title="请选择表示形式" title={translations[language].Title}
open={isOpen} open={isOpen}
footer={ footer={
<Button <Button
...@@ -51,7 +125,7 @@ export const PositionModal = () => { ...@@ -51,7 +125,7 @@ export const PositionModal = () => {
{positions.map((position, idx) => ( {positions.map((position, idx) => (
<CheckCard <CheckCard
key={idx} key={idx}
title={cardPositionToChinese(position)} title={cardPosition(position)}
value={position} value={position}
/> />
))} ))}
...@@ -60,20 +134,22 @@ export const PositionModal = () => { ...@@ -60,20 +134,22 @@ export const PositionModal = () => {
); );
}; };
function cardPositionToChinese(position: ygopro.CardPosition): string { // Function to get card position based on language
function cardPosition(position: ygopro.CardPosition): string {
const messages = translations[language];
switch (position) { switch (position) {
// TODO: i18n
case ygopro.CardPosition.FACEUP_ATTACK: { case ygopro.CardPosition.FACEUP_ATTACK: {
return "正面攻击形式"; return messages.FACEUP_ATTACK;
} }
case ygopro.CardPosition.FACEUP_DEFENSE: { case ygopro.CardPosition.FACEUP_DEFENSE: {
return "正面防守形式"; return messages.FACEUP_DEFENSE;
} }
case ygopro.CardPosition.FACEDOWN_ATTACK: { case ygopro.CardPosition.FACEDOWN_ATTACK: {
return "背面攻击形式"; return messages.FACEDOWN_ATTACK;
} }
case ygopro.CardPosition.FACEDOWN_DEFENSE: { case ygopro.CardPosition.FACEDOWN_DEFENSE: {
return "背面防守形式"; return messages.FACEDOWN_DEFENSE;
} }
default: { default: {
return "[?]"; return "[?]";
......
...@@ -2,6 +2,7 @@ import { CheckCard } from "@ant-design/pro-components"; ...@@ -2,6 +2,7 @@ import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Segmented, Space, Tooltip } from "antd"; import { Button, Card, Segmented, Space, Tooltip } from "antd";
import classnames from "classnames"; import classnames from "classnames";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio"; import { INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { type CardMeta, Region, type ygopro } from "@/api"; import { type CardMeta, Region, type ygopro } from "@/api";
...@@ -57,6 +58,8 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({ ...@@ -57,6 +58,8 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
const minMaxText = min === max ? min : `${min}-${max}`; const minMaxText = min === max ? min : `${min}-${max}`;
const { t: i18n } = useTranslation("SelectCardModal");
useEffect(() => { useEffect(() => {
const initial: [ygopro.CardZone, Option[]][] = grouped.map(([zone, _]) => [ const initial: [ygopro.CardZone, Option[]][] = grouped.map(([zone, _]) => [
zone, zone,
...@@ -107,8 +110,10 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({ ...@@ -107,8 +110,10 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
<> <>
<span>{preHintMsg}</span> <span>{preHintMsg}</span>
<span>{selectHintMsg}</span> <span>{selectHintMsg}</span>
<span>(请选择 {minMaxText} 张卡)</span> <span>
<span>{single ? "每次选择一张" : ""}</span> ({i18n("PleaseSelect")} {minMaxText} {i18n("Cards")})
</span>
<span>{single ? i18n("SelectOneCardAtTime") : ""}</span>
</> </>
} // TODO: 这里可以再细化一些 } // TODO: 这里可以再细化一些
width={"38.25rem"} width={"38.25rem"}
......
import { DownOutlined } from "@ant-design/icons"; import { DownOutlined } from "@ant-design/icons";
import { Button, Drawer, Input } from "antd"; import { Button, Drawer, Input } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { IconFont, ScrollableArea, useChat } from "@/ui/Shared"; import { IconFont, ScrollableArea, useChat } from "@/ui/Shared";
...@@ -17,7 +18,7 @@ interface ChatItem { ...@@ -17,7 +18,7 @@ interface ChatItem {
export const ChatBox: React.FC = () => { export const ChatBox: React.FC = () => {
const { open } = useSnapshot(store); const { open } = useSnapshot(store);
const { dialogs, input, setInput, ref, onSend } = useChat(true); const { dialogs, input, setInput, ref, onSend } = useChat(true);
const { t: i18n } = useTranslation("Chat");
const onClose = () => (store.open = false); const onClose = () => (store.open = false);
return ( return (
...@@ -42,7 +43,7 @@ export const ChatBox: React.FC = () => { ...@@ -42,7 +43,7 @@ export const ChatBox: React.FC = () => {
value={input} value={input}
onChange={(event) => setInput(event.target.value)} onChange={(event) => setInput(event.target.value)}
autoSize autoSize
placeholder="请输入聊天内容" placeholder={i18n("PleaseEnterChatContent")}
onPressEnter={(e) => { onPressEnter={(e) => {
e.preventDefault(); e.preventDefault();
onSend(); onSend();
......
...@@ -31,6 +31,8 @@ import { IconFont } from "@/ui/Shared"; ...@@ -31,6 +31,8 @@ import { IconFont } from "@/ui/Shared";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { useTranslation } from "react-i18next";
import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils"; import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils";
import { openChatBox } from "../ChatBox"; import { openChatBox } from "../ChatBox";
...@@ -38,6 +40,147 @@ const { useToken } = theme; ...@@ -38,6 +40,147 @@ const { useToken } = theme;
const FINISH_CANCEL_RESPONSE = -1; const FINISH_CANCEL_RESPONSE = -1;
// Define the possible language codes (I18N)
type Language = "en" | "br" | "pt" | "fr" | "ja" | "ko" | "es" | "cn";
// Define the structure for the messages (I18N)
const messages: Record<
Language,
{
drawPhase: string;
standbyPhase: string;
mainPhase1: string;
battlePhase: string;
battleStart: string;
battleStep: string;
damage: string;
damageCalc: string;
mainPhase2: string;
endPhase: string;
unknown: string;
}
> = {
en: {
drawPhase: "Draw",
standbyPhase: "Standhy Phase",
mainPhase1: "Main Phase 1",
battlePhase: "Battle Phase",
battleStart: "Battle Start",
battleStep: "Battle Step",
damage: "Damage Step",
damageCalc: "Damage Step (Damage Calculation)",
mainPhase2: "Main Phase 2",
endPhase: "End Phase",
unknown: "Unknown",
},
br: {
drawPhase: "Compra",
standbyPhase: "Fase de Espera",
mainPhase1: "Fase Principal 1",
battlePhase: "Fase de Batalha",
battleStart: "Início da Batalha",
battleStep: "Fase da Batalha",
damage: "Fase de Dano",
damageCalc: "Fase de Dano (Cálculo de Dano)",
mainPhase2: "Fase Principal 2",
endPhase: "Fase Final",
unknown: "Desconhecido",
},
pt: {
drawPhase: "Compra",
standbyPhase: "Fase de Espera",
mainPhase1: "Fase Principal 1",
battlePhase: "Fase de Batalha",
battleStart: "Início da Batalha",
battleStep: "Fase da Batalha",
damage: "Fase de Dano",
damageCalc: "Fase de Dano (Cálculo de Dano)",
mainPhase2: "Fase Principal 2",
endPhase: "Fase Final",
unknown: "Desconhecido",
},
fr: {
drawPhase: "Pioche",
standbyPhase: "Phase de Standby",
mainPhase1: "Phase Principale 1",
battlePhase: "Phase de Bataille",
battleStart: "Début de la Bataille",
battleStep: "Étape de Bataille",
damage: "Étape de Dégâts",
damageCalc: "Étape de Dégâts (Calcul des Dégâts)",
mainPhase2: "Phase Principale 2",
endPhase: "Phase Finale",
unknown: "Inconnu",
},
ja: {
drawPhase: "ドロー",
standbyPhase: "スタンバイフェイズ",
mainPhase1: "メインフェイズ 1",
battlePhase: "バトルフェイズ",
battleStart: "バトル開始",
battleStep: "バトルステップ",
damage: "ダメージステップ",
damageCalc: "ダメージステップ(ダメージ計算)",
mainPhase2: "メインフェイズ 2",
endPhase: "エンドフェイズ",
unknown: "未知",
},
ko: {
drawPhase: "드로우",
standbyPhase: "대기 페이즈",
mainPhase1: "메인 페이즈 1",
battlePhase: "배틀 페이즈",
battleStart: "배틀 시작",
battleStep: "배틀 스텝",
damage: "데미지 스텝",
damageCalc: "데미지 스텝 (데미지 계산)",
mainPhase2: "메인 페이즈 2",
endPhase: "엔드 페이즈",
unknown: "알 수 없음",
},
es: {
drawPhase: "Robo",
standbyPhase: "Fase de Espera",
mainPhase1: "Fase Principal 1",
battlePhase: "Fase de Batalla",
battleStart: "Inicio de Batalla",
battleStep: "Paso de Batalla",
damage: "Paso de Daño",
damageCalc: "Paso de Daño (Cálculo de Daño)",
mainPhase2: "Fase Principal 2",
endPhase: "Fase Final",
unknown: "Desconocido",
},
cn: {
drawPhase: "抽卡阶段",
standbyPhase: "准备阶段",
mainPhase1: "主要阶段 1",
battlePhase: "战斗阶段",
battleStart: "战斗开始",
battleStep: "战斗步骤",
damage: "伤害步骤",
damageCalc: "伤害步骤(伤害计算)",
mainPhase2: "主要阶段 2",
endPhase: "结束阶段",
unknown: "未知阶段",
},
};
// Get the language from localStorage or default to 'cn' (I18N)
const language = (localStorage.getItem("language") || "cn") as Language;
const drawPhase = messages[language].drawPhase;
const standbyPhase = messages[language].standbyPhase;
const mainPhase1 = messages[language].mainPhase1;
const battlePhase = messages[language].battlePhase;
const battleStart = messages[language].battleStart;
const battleStep = messages[language].battleStep;
const damage = messages[language].damage;
const damageCalc = messages[language].damageCalc;
const mainPhase2 = messages[language].mainPhase2;
const endPhase = messages[language].endPhase;
const unknown = messages[language].unknown;
/* End of definition (I18N) */
// PhaseType, 中文, response, 是否显示,是否禁用 // PhaseType, 中文, response, 是否显示,是否禁用
const initialPhaseBind: [ const initialPhaseBind: [
phase: PhaseType, phase: PhaseType,
...@@ -46,20 +189,21 @@ const initialPhaseBind: [ ...@@ -46,20 +189,21 @@ const initialPhaseBind: [
show: boolean, show: boolean,
disabled: boolean, disabled: boolean,
][] = [ ][] = [
[PhaseType.DRAW, "抽卡阶段", -1, true, true], [PhaseType.DRAW, drawPhase, -1, true, true],
[PhaseType.STANDBY, "准备阶段", -1, true, true], [PhaseType.STANDBY, standbyPhase, -1, true, true],
[PhaseType.MAIN1, "主要阶段 1", -1, true, true], [PhaseType.MAIN1, mainPhase1, -1, true, true],
[PhaseType.BATTLE, "战斗阶段", 6, true, false], [PhaseType.BATTLE, battlePhase, 6, true, false],
[PhaseType.BATTLE_START, "战斗开始", 3, false, true], [PhaseType.BATTLE_START, battleStart, 3, false, true],
[PhaseType.BATTLE_STEP, "战斗步骤", 3, false, true], [PhaseType.BATTLE_STEP, battleStep, 3, false, true],
[PhaseType.DAMAGE, "伤害步骤", 3, false, true], [PhaseType.DAMAGE, damage, 3, false, true],
[PhaseType.DAMAGE_GAL, "伤害步骤(伤害计算)", 3, false, true], [PhaseType.DAMAGE_GAL, damageCalc, 3, false, true],
[PhaseType.MAIN2, "主要阶段 2", 2, true, false], [PhaseType.MAIN2, mainPhase2, 2, true, false],
[PhaseType.END, "结束阶段", 7, true, false], [PhaseType.END, endPhase, 7, true, false],
[PhaseType.UNKNOWN, "未知阶段", -1, false, true], [PhaseType.UNKNOWN, unknown, -1, false, true],
]; ];
export const Menu = () => { export const Menu = () => {
const { t: i18n } = useTranslation("Menu");
const { const {
currentPlayer, currentPlayer,
chainSetting, chainSetting,
...@@ -121,10 +265,14 @@ export const Menu = () => { ...@@ -121,10 +265,14 @@ export const Menu = () => {
setPhaseSwitchItems(newPhaseSwitchItems); setPhaseSwitchItems(newPhaseSwitchItems);
}, [phaseBind]); }, [phaseBind]);
const allChain = language !== "cn" ? "All Chain" : "";
const ignoreChain = language !== "cn" ? "Ignore Chain" : "";
const smartChain = language !== "cn" ? "Smart Chain" : "";
const chainSettingTexts = [ const chainSettingTexts = [
[ChainSetting.CHAIN_ALL, "全部连锁"], [ChainSetting.CHAIN_ALL, allChain],
[ChainSetting.CHAIN_IGNORE, "忽略连锁"], [ChainSetting.CHAIN_IGNORE, ignoreChain],
[ChainSetting.CHAIN_SMART, "智能连锁"], [ChainSetting.CHAIN_SMART, smartChain],
] as const; ] as const;
const chainSettingItems: MenuProps["items"] = chainSettingTexts.map( const chainSettingItems: MenuProps["items"] = chainSettingTexts.map(
([key, text]) => ({ ([key, text]) => ({
...@@ -136,13 +284,12 @@ export const Menu = () => { ...@@ -136,13 +284,12 @@ export const Menu = () => {
}, },
}), }),
); );
const surrenderMenuItems: MenuProps["items"] = [ const surrenderMenuItems: MenuProps["items"] = [
{ {
label: "取消", label: i18n("Cancel"),
}, },
{ {
label: "确定", label: i18n("Confirm"),
danger: true, danger: true,
onClick: sendSurrender, onClick: sendSurrender,
}, },
...@@ -154,7 +301,7 @@ export const Menu = () => { ...@@ -154,7 +301,7 @@ export const Menu = () => {
<div className={styles["menu-container"]}> <div className={styles["menu-container"]}>
<SelectManager /> <SelectManager />
<DropdownWithTitle <DropdownWithTitle
title="请选择要进入的阶段" title={i18n("SelectPhase")}
menu={{ items: phaseSwitchItems }} menu={{ items: phaseSwitchItems }}
disabled={globalDisable} disabled={globalDisable}
> >
...@@ -176,7 +323,7 @@ export const Menu = () => { ...@@ -176,7 +323,7 @@ export const Menu = () => {
type="text" type="text"
></Button> ></Button>
</DropdownWithTitle> </DropdownWithTitle>
<Tooltip title="聊天室"> <Tooltip title={i18n("ChatRoom")}>
<Button <Button
icon={<MessageFilled />} icon={<MessageFilled />}
onClick={openChatBox} onClick={openChatBox}
...@@ -184,7 +331,7 @@ export const Menu = () => { ...@@ -184,7 +331,7 @@ export const Menu = () => {
></Button> ></Button>
</Tooltip> </Tooltip>
<DropdownWithTitle <DropdownWithTitle
title="是否投降?" title={i18n("DoYouSurrunder")}
menu={{ items: surrenderMenuItems }} menu={{ items: surrenderMenuItems }}
> >
<Button icon={<CloseCircleFilled />} type="text"></Button> <Button icon={<CloseCircleFilled />} type="text"></Button>
...@@ -246,6 +393,7 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({ ...@@ -246,6 +393,7 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({
}; };
const SelectManager: React.FC = () => { const SelectManager: React.FC = () => {
const { t: i18n } = useTranslation("Menu");
const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo); const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo);
const onFinishOrCancel = () => { const onFinishOrCancel = () => {
sendSelectSingleResponse(FINISH_CANCEL_RESPONSE); sendSelectSingleResponse(FINISH_CANCEL_RESPONSE);
...@@ -258,7 +406,7 @@ const SelectManager: React.FC = () => { ...@@ -258,7 +406,7 @@ const SelectManager: React.FC = () => {
disabled={!cancelable && !finishable} disabled={!cancelable && !finishable}
onClick={onFinishOrCancel} onClick={onFinishOrCancel}
> >
{finishable ? "完成选择" : "取消选择"} {finishable ? i18n("SelectionComplete") : i18n("Deselect")}
</Button> </Button>
</div> </div>
); );
......
...@@ -4,22 +4,124 @@ import { IconFont } from "@/ui/Shared"; ...@@ -4,22 +4,124 @@ import { IconFont } from "@/ui/Shared";
import CardPosition = ygopro.CardPosition; import CardPosition = ygopro.CardPosition;
// Define the possible language codes (I18N)
type Language = "en" | "br" | "pt" | "fr" | "ja" | "ko" | "es" | "cn";
// Define the structure for the messages (I18N)
const messages: Record<
Language,
{
sSet: string;
summon: string;
spSummon: string;
posChange: string;
mSet: string;
activate: string;
attack: string;
}
> = {
en: {
sSet: "Set",
summon: "Normal Summon",
spSummon: "Special Summon",
posChange: "Change Position",
mSet: "Set",
activate: "Activate",
attack: "Attack",
},
br: {
sSet: "Setar",
summon: "Invocação Normal",
spSummon: "Invocação Especial",
posChange: "Mudar Posição",
mSet: "Setar",
activate: "Ativar",
attack: "Atacar",
},
pt: {
sSet: "Setar",
summon: "Invocação Normal",
spSummon: "Invocação Especial",
posChange: "Mudar Posição",
mSet: "Setar",
activate: "Ativar",
attack: "Atacar",
},
fr: {
sSet: "Poser",
summon: "Invocation Normale",
spSummon: "Invocation Spéciale",
posChange: "Changer de Position",
mSet: "Poser",
activate: "Activer",
attack: "Attaquer",
},
ja: {
sSet: "セット",
summon: "通常召喚",
spSummon: "特殊召喚",
posChange: "表示形式変更",
mSet: "セット",
activate: "発動",
attack: "攻撃",
},
ko: {
sSet: "세트",
summon: "일반 소환",
spSummon: "특수 소환",
posChange: "포지션 변경",
mSet: "세트",
activate: "발동",
attack: "공격",
},
es: {
sSet: "Colocar",
summon: "Invocación Normal",
spSummon: "Invocación Especial",
posChange: "Cambiar Posición",
mSet: "Colocar",
activate: "Activar",
attack: "Atacar",
},
cn: {
sSet: "后场放置",
summon: "普通召唤",
spSummon: "特殊召唤",
posChange: "改变表示形式",
mSet: "前场放置",
activate: "发动效果",
attack: "攻击",
},
};
// Get the language from localStorage or default to 'cn' (I18N)
const language = (localStorage.getItem("language") || "cn") as Language;
/* End of definition (I18N) */
export function interactTypeToString(t: InteractType): string { export function interactTypeToString(t: InteractType): string {
const sSet = messages[language].sSet;
const summon = messages[language].summon;
const spSummon = messages[language].spSummon;
const posChange = messages[language].posChange;
const mSet = messages[language].mSet;
const activate = messages[language].activate;
const attack = messages[language].attack;
switch (t) { switch (t) {
case InteractType.SUMMON: case InteractType.SUMMON:
return "普通召唤"; return summon;
case InteractType.SP_SUMMON: case InteractType.SP_SUMMON:
return "特殊召唤"; return spSummon;
case InteractType.POS_CHANGE: case InteractType.POS_CHANGE:
return "改变表示形式"; return posChange;
case InteractType.MSET: case InteractType.MSET:
return "前场放置"; return mSet;
case InteractType.SSET: case InteractType.SSET:
return "后场放置"; return sSet;
case InteractType.ACTIVATE: case InteractType.ACTIVATE:
return "发动效果"; return activate;
case InteractType.ATTACK: case InteractType.ATTACK:
return "攻击"; return attack;
default: default:
return "未知选项"; return "未知选项";
} }
......
import React, { createContext, useContext, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
interface I18NContextType { interface I18NContextType {
language: string; language: string;
...@@ -10,12 +10,23 @@ const I18NContext = createContext<I18NContextType | undefined>(undefined); ...@@ -10,12 +10,23 @@ const I18NContext = createContext<I18NContextType | undefined>(undefined);
export const I18NProvider: React.FC<{ children: React.ReactNode }> = ({ export const I18NProvider: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => { }) => {
const [language, setLanguage] = useState<string>("cn"); // default language const [language, setLanguage] = useState<string>(() => {
// Get the language from localStorage if it exists, otherwise default to "cn"
return localStorage.getItem("language") || "cn";
});
const changeLanguage = (newLanguage: string) => { const changeLanguage = (newLanguage: string) => {
setLanguage(newLanguage); setLanguage(newLanguage);
localStorage.setItem("language", newLanguage);
//Force a reload of the page to apply the language change
window.location.reload();
}; };
useEffect(() => {
localStorage.setItem("language", language);
}, [language]);
return ( return (
<I18NContext.Provider value={{ language, changeLanguage }}> <I18NContext.Provider value={{ language, changeLanguage }}>
{children} {children}
......
import { Checkbox, Col, Row, Tooltip } from "antd";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { FlagIcon, FlagIconCode } from "react-flag-kit";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Select } from "@/ui/Shared"; import { useI18N } from "../I18NContext";
const languageOptions: { value: string; label: string; flag: FlagIconCode }[] =
[
{ value: "cn", label: "简体中文", flag: "CN" as FlagIconCode },
{ value: "en", label: "English", flag: "US" as FlagIconCode },
{ value: "fr", label: "Français", flag: "FR" as FlagIconCode },
{ value: "ja", label: "日本語", flag: "JP" as FlagIconCode },
{ value: "br", label: "Português do Brasil", flag: "BR" as FlagIconCode },
{ value: "pt", label: "Português", flag: "PT" as FlagIconCode },
{ value: "es", label: "Castellano", flag: "ES" as FlagIconCode },
];
export const I18NSelector: React.FC = () => { export const I18NSelector: React.FC = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const { language, changeLanguage } = useI18N();
const onClickLanguageChange = (language: any) => { const onClickLanguageChange = (selectedLanguage: string) => {
i18n.changeLanguage(language); changeLanguage(selectedLanguage);
i18n.changeLanguage(selectedLanguage);
}; };
useEffect(() => { useEffect(() => {
// Adding language state as a dependency to force re-render i18n.changeLanguage(language);
// when the language changes }, [language]);
}, [i18n.language]);
return ( return (
<Select <Row gutter={[16, 16]}>
value={i18n.language} {languageOptions.map((lang) => (
onChange={onClickLanguageChange} <Col key={lang.value}>
options={[ <Tooltip title={lang.label}>
{ value: "cn", label: "简体中文" }, <Checkbox
{ value: "en", label: "English" }, checked={i18n.language === lang.value}
{ value: "fr", label: "Français" }, onChange={() => onClickLanguageChange(lang.value)}
{ value: "jp", label: "日本語" }, >
{ value: "br", label: "português do Brasil" }, <FlagIcon code={lang.flag} size={24} />
{ value: "pt", label: "português" }, </Checkbox>
{ value: "es", label: "Castellano" }, </Tooltip>
]} </Col>
/> ))}
</Row>
); );
}; };
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
"Match": "Match", "Match": "Match",
"DeckBuilding": "Montagem de baralho", "DeckBuilding": "Montagem de baralho",
"PersonalCenter": "Centro pessoal", "PersonalCenter": "Centro pessoal",
"MyCardCommunity": "Comunidade Mengka", "MyCardCommunity": "Comunidade MyCard",
"DuelDatabase": "Base de dados de duelos", "DuelDatabase": "Base de dados de duelos",
"LogOut": "Sair", "LogOut": "Sair",
"Login": "Entrar no Mengka", "Login": "Entrar no MyCard",
"Fullscreen": "Tela cheia" "Fullscreen": "Tela cheia",
"SystemSettings": "Configurações do sistema"
}, },
"Start": { "Start": {
"Title": "Plataforma de batalha online de Yu-Gi-Oh!", "Title": "Plataforma de batalha online de Yu-Gi-Oh!",
...@@ -24,10 +25,10 @@ ...@@ -24,10 +25,10 @@
"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.", "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", "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.", "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", "MCCustomRoomTitle": "MyCard Sala Personalizada",
"MCCustomRoomDesc": "Crie ou entre em salas personalizadas no servidor e batalhe com amigos.", "MCCustomRoomDesc": "Crie ou entre em salas personalizadas no MyCard servidor e batalhe com amigos.",
"MCSpectatorListTitle": "Lista de espectadores", "MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no Mengka MyCard.", "MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no MyCard MyCard.",
"SinglePlayerModeTitle": "Modo de um jogador", "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.", "SinglePlayerModeDesc": "Inicie um duelo contra a IA no servidor Koishi 7210 para testar seu deck ou apenas passar o tempo.",
"CustomRoomTitle": "Sala Personalizada", "CustomRoomTitle": "Sala Personalizada",
...@@ -36,5 +37,166 @@ ...@@ -36,5 +37,166 @@
"ReplayDesc": "Assista livremente aos duelos passados e reviva aqueles momentos emocionantes de reversão.", "ReplayDesc": "Assista livremente aos duelos passados e reviva aqueles momentos emocionantes de reversão.",
"WIPTitle": "Em desenvolvimento...", "WIPTitle": "Em desenvolvimento...",
"WIPDesc": "Aguarde por outras funcionalidades." "WIPDesc": "Aguarde por outras funcionalidades."
},
"BuildDeck": {
"EnterTheDeckName": "Nome do deck",
"Shuffle": "Embaralhar",
"Sort": "Ordenar",
"Clear": "Limpar",
"Reset": "Redefinir",
"Save": "Salvar",
"QuestionCircleTooltip": "Clique duas vezes para adicionar uma carta, clique com o botão direito para remover uma carta, pressione o botão do meio do mouse para alternar cartas entre o deck principal e o deck lateral.",
"Filter": "Filtrar",
"SortBy": "Ordenar por ",
"KeywordsPlaceholder": "Palavras-chave (separadas por espaços)",
"FromNewToOld": "Do novo para o antigo",
"FromOldToNew": "Do antigo para o novo",
"AttackPowerFromHighToLow": "Poder de ataque do alto para o baixo",
"AttackPowerFromLowToHigh": "Poder de ataque do baixo para o alto",
"DefensePowerFromHighToLow": "Poder de defesa do alto para o baixo",
"DefensePowerFromLowToHigh": "Poder de defesa do baixo para o alto",
"StarsRanksLevelsLinkFromHighToLow": "Estrelas/Rank/Níveis/Link do alto para o baixo",
"StarsRanksLevelsLinkFromLowToHigh": "Estrelas/Rank/Níveis/Link do baixo para o alto",
"PendulumScaleFromHighToLow": "Escala Pêndulo do alto para o baixo",
"PendulumScaleFromLowToHigh": "Escala Pêndulo do baixo para o alto",
"ResetSuccessful": "Redefinição bem-sucedida",
"SaveSuccessful": "Salvo com sucesso",
"NoDeckGroupFound": "Nenhum grupo de deck correspondente encontrado",
"OnlyShowDecksIUploaded": "Mostrar apenas decks que eu enviei",
"ShowAllOnlineDecks": "Mostrar todos os decks online",
"CardDatabase": "Banco de Dados de Cartas",
"MDProOnlineDeck": "Deck Online MDPro"
},
"Filter": {
"CardFilter": "Filtro de Carta",
"Attribute": "Atributo",
"Race": "Raça",
"Type": "Tipo",
"Level": "Nível",
"PendulumScale": "Escala Pêndulo",
"Attack": "Ataque",
"Defense": "Defesa",
"Select": "-Selecione-",
"Minimum": "Min",
"Maximum": "Máx",
"Confirm": "Confirmar",
"Cancel": "Cancelar"
},
"CardDetails": {
"Level": "Nível",
"Type": "Tipo",
"Attribute": "Atributo",
"Race": "Raça",
"Attack": "Ataque",
"Defence": "Defesa",
"PendulumScale": "Escala Pêndulo",
"MonsterEffect": "Efeito de Monstro",
"PendulumEffect": "Efeito Pêndulo",
"CardEffect": "Efeito da Carta"
},
"WaitRoom": {
"Deck": "Deck",
"JoinDuelist": "Juntar-se ao Duelista",
"JoinSpectator": "Juntar-se como Espectador",
"DuelReady": "Pronto para Duelo",
"CancelReady": "Cancelar Prontidão",
"LeaveRoom": "Sair da Sala",
"Expand": "Expandir",
"Collapse": "Recolher",
"Sidebar": "Barra Lateral",
"StartGame": "Iniciar Jogo",
"PlsRockPaperScissors": "Por favor, jogue Pedra, Papel, Tesoura",
"WaitOpponentPlayRockPaperScissors": "Aguarde o oponente jogar Pedra, Papel, Tesoura",
"PlsChooseWhoGoesFirst": "Por favor, escolha quem começa",
"WaitingForGameToStart": "Aguardando o início do jogo",
"Scissors": "Tesoura",
"Rock": "Pedra",
"Paper": "Papel"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "Criar/Entrar em Sala Privada",
"RoomPassword": "Senha",
"Initial": "Inicial ",
"InitialHandSize": "Tamanho da Mão Inicial",
"DrawPerTurn": "Compras por Turno",
"CardsAllowed": "Cartas Permitidas",
"SimplifiedChinese": "Chinês Simplificado",
"CustomCards": "Cartas Personalizadas",
"ExclusiveCardsProhibited": "Cartas Exclusivas Proibidas",
"AllCards": "Todas as Cartas",
"DuelMode": "Modo de Duelo",
"SingleMatchMode": "Modo de Partida Única",
"TournamentMode": "Modo de Torneio",
"DuelRules": "Regras de Duelo",
"MasterRule1": "Regra Mestre 1",
"MasterRule2": "Regra Mestre 2",
"MasterRule3": "Regra Mestre 3",
"NewMasterRule": "Nova Regra Mestre",
"MasterRule2020": "Regra Mestre 2020",
"NoDeckCheck": "Sem Verificação de Deck",
"NoShuffleDeck": "Sem Embaralhar Deck",
"40MinutesAutomaticOvertime": "40 Minutos de Tempo Extra Automático",
"EnterYourFriendsPrivateRoomPassword": "Insira a senha da sala privada do seu amigo aqui.",
"CreatePrivateRoom": "Criar Sala Privada",
"JoinPrivateRoom": "Entrar em Sala Privada"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "Procurar Sala pelo Nome do Jogador",
"RankedMatch": "Partida Ranqueada",
"Versus": "vs"
},
"DeckSelect": {
"CopySuccessful": "Cópia bem-sucedida",
"CopyFailed.": "Falha na cópia",
"CreateNewDeck": "Criar novo deck",
"ImportFromLocalFile": "Importar de arquivo local",
"ImportFromClipboard": "Importar da área de transferência",
"ClickOrDragFilesHereToUpload": "Clique ou arraste arquivos aqui para fazer upload",
"SupportsYdkExtension": "Suporta apenas arquivos de deck com a extensão .ydk.",
"UnableToReadClipboardContent": "Não foi possível ler o conteúdo da área de transferência."
},
"Chat": {
"PleaseEnterChatContent": "Por favor, insira o conteúdo do chat"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "Por favor, insira as informações da sala personalizada",
"Server": "Servidor",
"KoishiServer": "Servidor Koishi",
"UltraPreemptiveServer": "Servidor Ultra Preemptivo",
"PlayerNickname": "Apelido do Jogador",
"RoomPasswordOptional": "Senha da Sala (opcional)",
"JoinRoom": "Entrar na Sala"
},
"ReplayModal": {
"SelectReplay": "Selecionar Replay",
"ClickOrDragFilesHereToUpload": "Clique ou arraste arquivos aqui para fazer upload",
"SupportsYrd3dExtension": "Apenas arquivos de replay com a extensão .yrp3d são suportados.",
"StartReplay": "Iniciar Replay",
"PleaseUploadReplayFile": "Por favor, carregue o arquivo de replay primeiro."
},
"Popover": {
"First": "Primeiro",
"Second": "Segundo"
},
"Menu": {
"DoYouSurrunder": "Você se rende?",
"Cancel": "Cancelar",
"Confirm": "Confirmar",
"SelectPhase": "Por favor, selecione a fase",
"Deselect": "Deselecionar",
"SelectionComplete": "Seleção completa",
"ChatRoom": "Sala de Chat"
},
"SelectCardModal": {
"PleaseSelect": "Por favor, selecione",
"Cards": "Cartas",
"SelectOneCardAtTime": "Selecione uma carta por vez"
},
"SystemSettings": {
"AudioSettings": "Configurações de áudio",
"TurnOnMusic": "Música",
"TurnOnSoundEffects": "Efeitos sonoros",
"SwitchMusicAccordingToTheEnvironment": "Trocar música conforme o ambiente",
"LanguageSettings": "Idioma"
} }
} }
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"DuelDatabase": "决斗数据库", "DuelDatabase": "决斗数据库",
"LogOut": "退出登录", "LogOut": "退出登录",
"Login": "登录萌卡", "Login": "登录萌卡",
"Fullscreen": "全屏" "Fullscreen": "全屏",
"SystemSettings": "系统设置"
}, },
"Start": { "Start": {
"Title": "游戏王网页端对战平台", "Title": "游戏王网页端对战平台",
...@@ -35,6 +36,169 @@ ...@@ -35,6 +36,169 @@
"ReplayTitle": "录像回放", "ReplayTitle": "录像回放",
"ReplayDesc": "自由查看进行过的决斗,回味那些精彩的逆转瞬间。", "ReplayDesc": "自由查看进行过的决斗,回味那些精彩的逆转瞬间。",
"WIPTitle": "开发中...", "WIPTitle": "开发中...",
"WIPDesc": "其他功能敬请期待。" "WIPDesc": "其他功能敬请期待。",
"EnterSpectatorMode": "进入观战",
"PleaseSelectTheRoomToSpectate": "请选择观战的房间"
},
"BuildDeck": {
"EnterTheDeckName": "请输入卡组名字",
"Shuffle": "打乱",
"Sort": "排序",
"Clear": "清空",
"Reset": "重置",
"Save": "保存",
"QuestionCircleTooltip": "双击添加卡片,单击右键删除卡片,按下滑轮在主卡组和副卡组之间切换卡片",
"Filter": "筛选",
"SortBy": "排列 ",
"KeywordsPlaceholder": "关键词(空格分隔)",
"FromNewToOld": "从新到旧",
"FromOldToNew": "从旧到新",
"AttackPowerFromHighToLow": "攻击力从高到低",
"AttackPowerFromLowToHigh": "攻击力从低到高",
"DefensePowerFromHighToLow": "守备力从高到低",
"DefensePowerFromLowToHigh": "守备力从低到高",
"StarsRanksLevelsLinkFromHighToLow": "星/阶/刻/Link从高到低",
"StarsRanksLevelsLinkFromLowToHigh": "星/阶/刻/Link从低到高",
"PendulumScaleFromHighToLow": "灵摆刻度从高到低",
"PendulumScaleFromLowToHigh": "灵摆刻度从低到高",
"ResetSuccessful": "重置成功",
"SaveSuccessful": "保存成功",
"NoDeckGroupFound": "找不到相应卡组",
"OnlyShowDecksIUploaded": "只显示我上传的卡组",
"ShowAllOnlineDecks": "显示全部在线卡组",
"CardDatabase": "卡片数据库",
"MDProOnlineDeck": "Mdpro在线卡组"
},
"Filter": {
"CardFilter": "卡片筛选",
"Attribute": "属性",
"Race": "种族",
"Type": "类型",
"Level": "星级",
"PendulumScale": "灵摆刻度",
"Attack": "攻击力",
"Defense": "守备力",
"Select": "请选择",
"Minimum": "最小值",
"Maximum": "最大值",
"Confirm": "确 定",
"Cancel": "取 消"
},
"CardDetails": {
"Level": "等级",
"Type": "类型",
"Attribute": "类型",
"Race": "种族",
"Attack": "攻击力",
"Defence": "守备力",
"PendulumScale": "灵摆刻度",
"MonsterEffect": "怪兽效果",
"PendulumEffect": "灵摆效果",
"CardEffect": "卡片效果"
},
"WaitRoom": {
"Deck": "卡组",
"JoinDuelist": "加入决斗者",
"JoinSpectator": "加入观战",
"DuelReady": "决斗准备",
"CancelReady": "取消准备",
"LeaveRoom": "退出房间",
"Expand": "展开",
"Collapse": "收起",
"Sidebar": "侧栏",
"StartGame": "开始游戏",
"PlsRockPaperScissors": "请猜拳",
"WaitOpponentPlayRockPaperScissors": "等待对方猜拳",
"PlsChooseWhoGoesFirst": "请选择先后手",
"WaitingForGameToStart": "等待游戏开始",
"Scissors": "剪刀",
"Rock": "石头",
"Paper": "布"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "创建/加入私密房间",
"RoomPassword": "房间密码",
"Initial": "初始 ",
"InitialHandSize": "初始手牌数",
"DrawPerTurn": "每回合抽卡",
"CardsAllowed": "卡片允许",
"SimplifiedChinese": "简体中文",
"CustomCards": "自制卡",
"ExclusiveCardsProhibited": "专有卡禁止",
"AllCards": "所有卡片",
"DuelMode": "决斗模式",
"SingleMatchMode": "单局模式",
"TournamentMode": "比赛模式",
"DuelRules": "决斗规则",
"MasterRule1": "大师规则1",
"MasterRule2": "大师规则2",
"MasterRule3": "大师规则3",
"NewMasterRule": "新大师规则",
"MasterRule2020": "大师规则2020",
"NoDeckCheck": "不检查卡组",
"NoShuffleDeck": "不切洗卡组",
"40MinutesAutomaticOvertime": "40分自动加时",
"EnterYourFriendsPrivateRoomPassword": "在这输入你朋友的私密房间密码",
"CreatePrivateRoom": "创建私密房间",
"JoinPrivateRoom": "加入私密房间"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "通过玩家用户名搜索房间",
"RankedMatch": "竞技匹配",
"Versus": "与"
},
"DeckSelect": {
"CopySuccessful": "复制成功",
"CopyFailed": "复制失败",
"CreateNewDeck": "新建卡组",
"ImportFromLocalFile": "从本地文件导入",
"ImportFromClipboard": "从剪贴板导入",
"ClickOrDragFilesHereToUpload": "单击或拖动文件到此区域进行上传",
"SupportsYdkExtension": "仅支持后缀名为ydk的卡组文件。",
"UnableToReadClipboardContent": "无法读取剪贴板内容:"
},
"Chat": {
"PleaseEnterChatContent": "请输入聊天内容"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "请输入自定义房间信息",
"Server": "服务器",
"KoishiServer": "Koishi服",
"UltraPreemptiveServer": "超先行服",
"PlayerNickname": "玩家昵称",
"RoomPasswordOptional": "房间密码(可选)",
"JoinRoom": "加入房间"
},
"ReplayModal": {
"SelectReplay": "选择回放",
"ClickOrDragFilesHereToUpload": "单击或拖动文件到此区域进行上传",
"SupportsYrd3dExtension": "仅支持后缀名为yrp3d的录像文件。",
"StartReplay": "开始回放",
"PleaseUploadReplayFile": "请先上传录像文件"
},
"Popover": {
"First": "先手",
"Second": "后手"
},
"Menu": {
"DoYouSurrunder": "是否投降?",
"Cancel": "取消",
"Confirm": "确定",
"SelectPhase": "请选择要进入的阶段",
"Deselect": "取消选择",
"SelectionComplete": "完成选择",
"ChatRoom": "聊天室"
},
"SelectCardModal": {
"PleaseSelect": "请选择",
"Cards": "张卡",
"SelectOneCardAtTime": "每次选择一张"
},
"SystemSettings": {
"AudioSettings": "音频设置",
"TurnOnMusic": "开启音乐",
"TurnOnSoundEffects": "开启音效",
"SwitchMusicAccordingToTheEnvironment": "根据环境切换音乐",
"LanguageSettings": "语言"
} }
} }
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
"Match": "Match", "Match": "Match",
"DeckBuilding": "Deck Building", "DeckBuilding": "Deck Building",
"PersonalCenter": "Personal Center", "PersonalCenter": "Personal Center",
"MyCardCommunity": "Mengka Community", "MyCardCommunity": "MyCard Community",
"DuelDatabase": "Duel Database", "DuelDatabase": "Duel Database",
"LogOut": "Log out", "LogOut": "Log out",
"Login": "Login to Mengka", "Login": "Login to MyCard",
"Fullscreen": "Fullscreen" "Fullscreen": "Fullscreen",
"SystemSettings": "System Settings"
}, },
"Start": { "Start": {
"Title": "Yu-Gi-Oh! Web Based Battle Platform", "Title": "Yu-Gi-Oh! Web Based Battle Platform",
...@@ -24,10 +25,10 @@ ...@@ -24,10 +25,10 @@
"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.", "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", "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.", "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", "MCCustomRoomTitle": "MyCard Custom Room",
"MCCustomRoomDesc": "Create or join custom rooms on MC server and battle with friends.", "MCCustomRoomDesc": "Create or join custom rooms on MC server and battle with friends.",
"MCSpectatorListTitle": "Spectator List", "MCSpectatorListTitle": "Spectator List",
"MCSpectatorListDesc": "Watch the duels currently taking place on Mengka MyCard.", "MCSpectatorListDesc": "Watch the duels currently taking place on MyCard MyCard.",
"SinglePlayerModeTitle": "Single Player Mode", "SinglePlayerModeTitle": "Single Player Mode",
"SinglePlayerModeDesc": "Start a duel against AI on Koishi 7210 server to test your deck or just pass the time.", "SinglePlayerModeDesc": "Start a duel against AI on Koishi 7210 server to test your deck or just pass the time.",
"CustomRoomTitle": "Custom Room", "CustomRoomTitle": "Custom Room",
...@@ -35,6 +36,169 @@ ...@@ -35,6 +36,169 @@
"ReplayTitle": "Replay", "ReplayTitle": "Replay",
"ReplayDesc": "Freely watch past duels and relive those exciting moments of reversal.", "ReplayDesc": "Freely watch past duels and relive those exciting moments of reversal.",
"WIPTitle": "Under development...", "WIPTitle": "Under development...",
"WIPDesc": "Stay tuned for other features." "WIPDesc": "Stay tuned for other features.",
"EnterSpectatorMode": "Enter Spectator Mode",
"PleaseSelectTheRoomToSpectate": "Please select the room to spectate"
},
"BuildDeck": {
"EnterTheDeckName": "Deck name",
"Shuffle": "Shuffle",
"Sort": "Sort",
"Clear": "Clear",
"Reset": "Reset",
"Save": "Save",
"QuestionCircleTooltip": "Double-click to add a card, right-click to remove a card, press the mouse wheel to switch cards between the main deck and the side deck.",
"Filter": "Filter",
"SortBy": "Sort By ",
"KeywordsPlaceholder": "Keywords (separated by spaces)",
"FromNewToOld": "From new to old",
"FromOldToNew": "From old to new",
"AttackPowerFromHighToLow": "Attack power from high to low",
"AttackPowerFromLowToHigh": "Attack power from low to high",
"DefensePowerFromHighToLow": "Defense power from high to low",
"DefensePowerFromLowToHigh": "Defense power from low to high",
"StarsRanksLevelsLinkFromHighToLow": "Stars/Ranks/Levels/Link from high to low",
"StarsRanksLevelsLinkFromLowToHigh": "Stars/Ranks/Levels/Link from low to high",
"PendulumScaleFromHighToLow": "Pendulum Scale from high to low",
"PendulumScaleFromLowToHigh": "Pendulum Scale from low to high",
"ResetSuccessful": "Reset successful",
"SaveSuccessful": "Save successful",
"NoDeckGroupFound": "No corresponding Deck group found",
"OnlyShowDecksIUploaded": "Only show decks I uploaded",
"ShowAllOnlineDecks": "Show all online decks",
"CardDatabase": "Card Database",
"MDProOnlineDeck": "MDPro Online Deck"
},
"Filter": {
"CardFilter": "Card Filter",
"Attribute": "Attribute",
"Race": "Race",
"Type": "Type",
"Level": "Level",
"PendulumScale": "Pendulum Scale",
"Attack": "Attack",
"Defense": "Defense",
"Select": "-Select-",
"Minimum": "Min",
"Maximum": "Max",
"Confirm": "Confirm",
"Cancel": "Cancel"
},
"CardDetails": {
"Level": "Level",
"Type": "Type",
"Attribute": "Attribute",
"Race": "Race",
"Attack": "Attack",
"Defence": "Defence",
"PendulumScale": "Pendulum Scale",
"MonsterEffect": "Monster Effect",
"PendulumEffect": "Pendulum Effect",
"CardEffect": "Card Effect"
},
"WaitRoom": {
"Deck": "Deck",
"JoinDuelist": "Join Duelist",
"JoinSpectator": "Join Spectator",
"DuelReady": "Duel Ready",
"CancelReady": "Cancel Ready",
"LeaveRoom": "Leave Room",
"Expand": "Expand",
"Collapse": "Collapse",
"Sidebar": "Sidebar",
"StartGame": "Start Game",
"PlsRockPaperScissors": "Please play Rock, Paper, Scissors",
"WaitOpponentPlayRockPaperScissors": "Wait for the opponent to play Rock, Paper, Scissors",
"PlsChooseWhoGoesFirst": "Please choose who goes first",
"WaitingForGameToStart": "Waiting for the game to start",
"Scissors": "Scissors",
"Rock": "Rock",
"Paper": "Paper"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "Create/Join Private Room",
"RoomPassword": "Password",
"Initial": "Initial ",
"InitialHandSize": "Initial Hand Size",
"DrawPerTurn": "Draw per Turn",
"CardsAllowed": "Cards Allowed",
"SimplifiedChinese": "Simplified Chinese",
"CustomCards": "Custom Cards",
"ExclusiveCardsProhibited": "Exclusive Cards Prohibited",
"AllCards": "All Cards",
"DuelMode": "Duel Mode",
"SingleMatchMode": "Single Match Mode",
"TournamentMode": "Tournament Mode",
"DuelRules": "Duel Rules",
"MasterRule1": "Master Rule 1",
"MasterRule2": "Master Rule 2",
"MasterRule3": "Master Rule 3",
"NewMasterRule": "New Master Rule",
"MasterRule2020": "Master Rule 2020",
"NoDeckCheck": "No Deck Check",
"NoShuffleDeck": "No Shuffle Deck",
"40MinutesAutomaticOvertime": "40 Minutes Automatic Overtime",
"EnterYourFriendsPrivateRoomPassword": "Enter your friend's private room password here.",
"CreatePrivateRoom": "Create Private Room",
"JoinPrivateRoom": "Join Private Room"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "Search Room by Player Username",
"RankedMatch": "Ranked Match",
"Versus": "vs"
},
"DeckSelect": {
"CopySuccessful": "Copy successful",
"CopyFailed.": "Copy failed",
"CreateNewDeck": "Create new deck",
"ImportFromLocalFile": "Import from local file",
"ImportFromClipboard": "Import from clipboard",
"ClickOrDragFilesHereToUpload": "Click or drag files here to upload",
"SupportsYdkExtension": "Only supports deck files with the .ydk extension.",
"UnableToReadClipboardContent": "Unable to read clipboard content."
},
"Chat": {
"PleaseEnterChatContent": "Please enter chat"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "Please enter custom room information",
"Server": "Server",
"KoishiServer": "Koishi Server",
"UltraPreemptiveServer": "Ultra Preemptive Server",
"PlayerNickname": "Player Nickname",
"RoomPasswordOptional": "Room Password (optional)",
"JoinRoom": "Join the Room"
},
"ReplayModal": {
"SelectReplay": "Select Replay",
"ClickOrDragFilesHereToUpload": "Click or drag files here to upload",
"SupportsYrd3dExtension": "Only replay files with the .yrp3d extension are supported.",
"StartReplay": "Start Replay",
"PleaseUploadReplayFile": "Please upload the replay file first."
},
"Popover": {
"First": "First",
"Second": "Second"
},
"Menu": {
"DoYouSurrunder": "Do you surrender?",
"Cancel": "Cancel",
"Confirm": "Confirm",
"SelectPhase": "Please select the phase",
"Deselect": "Deselect",
"SelectionComplete": "Selection complete",
"ChatRoom": "Chat Room"
},
"SelectCardModal": {
"PleaseSelect": "Please select",
"Cards": "Cards",
"SelectOneCardAtTime": "Select one card at a time"
},
"SystemSettings": {
"AudioSettings": "Audio",
"TurnOnMusic": "Music",
"TurnOnSoundEffects": "Sound effects",
"SwitchMusicAccordingToTheEnvironment": "Switch music according to the environment",
"LanguageSettings": "Language"
} }
} }
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
"Match": "Correspondance", "Match": "Correspondance",
"DeckBuilding": "Construction de Deck", "DeckBuilding": "Construction de Deck",
"PersonalCenter": "Centre personnel", "PersonalCenter": "Centre personnel",
"MyCardCommunity": "Communauté Mengka", "MyCardCommunity": "Communauté MyCard",
"DuelDatabase": "Base de données de duels", "DuelDatabase": "Base de données de duels",
"LogOut": "Déconnexion", "LogOut": "Déconnexion",
"Login": "Connexion à Mengka", "Login": "Connexion à MyCard",
"Fullscreen": "Plein écran" "Fullscreen": "Plein écran",
"SystemSettings": "Paramètres du système"
}, },
"Start": { "Start": {
"Title": "Plateforme de combat Yu-gi-oh basée sur le Web", "Title": "Plateforme de combat Yu-gi-oh basée sur le Web",
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
"MCCustomRoomTitle": "Salle personnalisée MC", "MCCustomRoomTitle": "Salle personnalisée MC",
"MCCustomRoomDesc": "Créez ou rejoignez des salles personnalisées sur le serveur MC et affrontez vos amis.", "MCCustomRoomDesc": "Créez ou rejoignez des salles personnalisées sur le serveur MC et affrontez vos amis.",
"MCSpectatorListTitle": "Liste des spectateurs", "MCSpectatorListTitle": "Liste des spectateurs",
"MCSpectatorListDesc": "Regardez les duels en cours sur Mengka MyCard.", "MCSpectatorListDesc": "Regardez les duels en cours sur MyCard MyCard.",
"SinglePlayerModeTitle": "Mode solo", "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.", "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", "CustomRoomTitle": "Salle personnalisée",
...@@ -36,5 +37,166 @@ ...@@ -36,5 +37,166 @@
"ReplayDesc": "Regardez librement les duels passés et revivez ces moments passionnants de retournement.", "ReplayDesc": "Regardez librement les duels passés et revivez ces moments passionnants de retournement.",
"WIPTitle": "En développement...", "WIPTitle": "En développement...",
"WIPDesc": "D'autres fonctionnalités à venir." "WIPDesc": "D'autres fonctionnalités à venir."
},
"BuildDeck": {
"EnterTheDeckName": "Nom du deck",
"Shuffle": "Mélanger",
"Sort": "Trier",
"Clear": "Effacer",
"Reset": "Réinitialiser",
"Save": "Enregistrer",
"QuestionCircleTooltip": "Double-cliquez pour ajouter une carte, cliquez avec le bouton droit pour retirer une carte, appuyez sur la molette de la souris pour basculer les cartes entre le deck principal et le deck latéral.",
"Filter": "Filtrer",
"SortBy": "Trier par ",
"KeywordsPlaceholder": "Mots-clés (séparés par des espaces)",
"FromNewToOld": "Du nouveau au vieux",
"FromOldToNew": "Du vieux au nouveau",
"AttackPowerFromHighToLow": "Puissance d'attaque de haut en bas",
"AttackPowerFromLowToHigh": "Puissance d'attaque de bas en haut",
"DefensePowerFromHighToLow": "Puissance de défense de haut en bas",
"DefensePowerFromLowToHigh": "Puissance de défense de bas en haut",
"StarsRanksLevelsLinkFromHighToLow": "Étoiles/Niveaux/Link de haut en bas",
"StarsRanksLevelsLinkFromLowToHigh": "Étoiles/Niveaux/Link de bas en haut",
"PendulumScaleFromHighToLow": "Échelle Pendule de haut en bas",
"PendulumScaleFromLowToHigh": "Échelle Pendule de bas en haut",
"ResetSuccessful": "Réinitialisation réussie",
"SaveSuccessful": "Enregistrement réussi",
"NoDeckGroupFound": "Aucun groupe de deck correspondant trouvé",
"OnlyShowDecksIUploaded": "Afficher seulement les decks que j'ai téléchargés",
"ShowAllOnlineDecks": "Afficher tous les decks en ligne",
"CardDatabase": "Base de données des cartes",
"MDProOnlineDeck": "Deck en ligne MDPro"
},
"Filter": {
"CardFilter": "Filtre de carte",
"Attribute": "Attribut",
"Race": "Race",
"Type": "Type",
"Level": "Niveau",
"PendulumScale": "Échelle Pendule",
"Attack": "Attaque",
"Defense": "Défense",
"Select": "-Sélectionner-",
"Minimum": "Min",
"Maximum": "Max",
"Confirm": "Confirmer",
"Cancel": "Annuler"
},
"CardDetails": {
"Level": "Niveau",
"Type": "Type",
"Attribute": "Attribut",
"Race": "Race",
"Attack": "Attaque",
"Defence": "Défense",
"PendulumScale": "Échelle Pendule",
"MonsterEffect": "Effet Monstre",
"PendulumEffect": "Effet Pendule",
"CardEffect": "Effet de Carte"
},
"WaitRoom": {
"Deck": "Deck",
"JoinDuelist": "Rejoindre le duelliste",
"JoinSpectator": "Rejoindre en tant que spectateur",
"DuelReady": "Prêt pour le duel",
"CancelReady": "Annuler la préparation",
"LeaveRoom": "Quitter la salle",
"Expand": "Développer",
"Collapse": "Réduire",
"Sidebar": "Barre latérale",
"StartGame": "Commencer le jeu",
"PlsRockPaperScissors": "Veuillez jouer à Pierre, Papier, Ciseaux",
"WaitOpponentPlayRockPaperScissors": "Attendez que l'adversaire joue à Pierre, Papier, Ciseaux",
"PlsChooseWhoGoesFirst": "Veuillez choisir qui commence",
"WaitingForGameToStart": "En attente du début du jeu",
"Scissors": "Ciseaux",
"Rock": "Pierre",
"Paper": "Papier"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "Créer/Rejoindre une salle privée",
"RoomPassword": "Mot de passe",
"Initial": "Initial ",
"InitialHandSize": "Taille de la main initiale",
"DrawPerTurn": "Piocher par tour",
"CardsAllowed": "Cartes autorisées",
"SimplifiedChinese": "Chinois simplifié",
"CustomCards": "Cartes personnalisées",
"ExclusiveCardsProhibited": "Cartes exclusives interdites",
"AllCards": "Toutes les cartes",
"DuelMode": "Mode Duel",
"SingleMatchMode": "Mode Match unique",
"TournamentMode": "Mode Tournoi",
"DuelRules": "Règles de Duel",
"MasterRule1": "Règle Maître 1",
"MasterRule2": "Règle Maître 2",
"MasterRule3": "Règle Maître 3",
"NewMasterRule": "Nouvelle Règle Maître",
"MasterRule2020": "Règle Maître 2020",
"NoDeckCheck": "Pas de vérification du deck",
"NoShuffleDeck": "Pas de mélange du deck",
"40MinutesAutomaticOvertime": "40 minutes de prolongation automatique",
"EnterYourFriendsPrivateRoomPassword": "Entrez le mot de passe de la salle privée de votre ami ici.",
"CreatePrivateRoom": "Créer une salle privée",
"JoinPrivateRoom": "Rejoindre une salle privée"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "Rechercher une salle par le nom d'utilisateur du joueur",
"RankedMatch": "Match classé",
"Versus": "vs"
},
"DeckSelect": {
"CopySuccessful": "Copie réussie",
"CopyFailed.": "Échec de la copie",
"CreateNewDeck": "Créer un nouveau deck",
"ImportFromLocalFile": "Importer depuis un fichier local",
"ImportFromClipboard": "Importer depuis le presse-papiers",
"ClickOrDragFilesHereToUpload": "Cliquez ou faites glisser les fichiers ici pour les télécharger",
"SupportsYdkExtension": "Prend en charge uniquement les fichiers de deck avec l'extension .ydk.",
"UnableToReadClipboardContent": "Impossible de lire le contenu du presse-papiers."
},
"Chat": {
"PleaseEnterChatContent": "Veuillez entrer le contenu du chat"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "Veuillez entrer les informations de la salle personnalisée",
"Server": "Serveur",
"KoishiServer": "Serveur Koishi",
"UltraPreemptiveServer": "Serveur Ultra Préemptif",
"PlayerNickname": "Pseudo du joueur",
"RoomPasswordOptional": "Mot de passe de la salle (optionnel)",
"JoinRoom": "Rejoindre la salle"
},
"ReplayModal": {
"SelectReplay": "Sélectionner le replay",
"ClickOrDragFilesHereToUpload": "Cliquez ou faites glisser les fichiers ici pour les télécharger",
"SupportsYrd3dExtension": "Seuls les fichiers de replay avec l'extension .yrp3d sont pris en charge.",
"StartReplay": "Démarrer le replay",
"PleaseUploadReplayFile": "Veuillez d'abord télécharger le fichier de replay."
},
"Popover": {
"First": "Premier",
"Second": "Deuxième"
},
"Menu": {
"DoYouSurrunder": "Abandonnez-vous?",
"Cancel": "Annuler",
"Confirm": "Confirmer",
"SelectPhase": "Veuillez sélectionner la phase",
"Deselect": "Désélectionner",
"SelectionComplete": "Sélection terminée",
"ChatRoom": "Salle de chat"
},
"SelectCardModal": {
"PleaseSelect": "Veuillez sélectionner",
"Cards": "Cartes",
"SelectOneCardAtTime": "Sélectionnez une carte à la fois"
},
"SystemSettings": {
"AudioSettings": "Paramètres audio",
"TurnOnMusic": "Activer la musique",
"TurnOnSoundEffects": "Effets sonores",
"SwitchMusicAccordingToTheEnvironment": "Changer la musique en fonction de l'environnement",
"LanguageSettings": "Langue"
} }
} }
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"DuelDatabase": "デュエルデータベース", "DuelDatabase": "デュエルデータベース",
"LogOut": "ログアウト", "LogOut": "ログアウト",
"Login": "萌卡にログインする", "Login": "萌卡にログインする",
"Fullscreen": "フルスクリーン" "Fullscreen": "フルスクリーン",
"SystemSettings": "システム設定"
}, },
"Start": { "Start": {
"Title": "遊戯王ウェブベースのバトルプラットフォーム", "Title": "遊戯王ウェブベースのバトルプラットフォーム",
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
"MCCustomRoomTitle": "MCカスタムルーム", "MCCustomRoomTitle": "MCカスタムルーム",
"MCCustomRoomDesc": "MCサーバーでカスタムルームを作成または参加し、友達と対戦しましょう。", "MCCustomRoomDesc": "MCサーバーでカスタムルームを作成または参加し、友達と対戦しましょう。",
"MCSpectatorListTitle": "MC観戦リスト", "MCSpectatorListTitle": "MC観戦リスト",
"MCSpectatorListDesc": "Mengka MyCardで現在進行中のデュエルを観戦しましょう。", "MCSpectatorListDesc": "MyCard MyCardで現在進行中のデュエルを観戦しましょう。",
"SinglePlayerModeTitle": "シングルプレイヤーモード", "SinglePlayerModeTitle": "シングルプレイヤーモード",
"SinglePlayerModeDesc": "Koishi 7210サーバーでAIとのデュエルを開始し、自分のデッキをテストしたり、ただ時間を潰したりします。", "SinglePlayerModeDesc": "Koishi 7210サーバーでAIとのデュエルを開始し、自分のデッキをテストしたり、ただ時間を潰したりします。",
"CustomRoomTitle": "カスタムルーム", "CustomRoomTitle": "カスタムルーム",
...@@ -36,5 +37,166 @@ ...@@ -36,5 +37,166 @@
"ReplayDesc": "過去のデュエルを自由に見て、それらのエキサイティングな逆転の瞬間を振り返りましょう。", "ReplayDesc": "過去のデュエルを自由に見て、それらのエキサイティングな逆転の瞬間を振り返りましょう。",
"WIPTitle": "開発中...", "WIPTitle": "開発中...",
"WIPDesc": "他の機能をお楽しみに。" "WIPDesc": "他の機能をお楽しみに。"
},
"BuildDeck": {
"EnterTheDeckName": "デッキ名を入力",
"Shuffle": "シャッフル",
"Sort": "ソート",
"Clear": "クリア",
"Reset": "リセット",
"Save": "保存",
"QuestionCircleTooltip": "カードを追加するにはダブルクリック、カードを削除するには右クリック、メインデッキとサイドデッキの間でカードを切り替えるにはホイールボタンを押します。",
"Filter": "フィルター",
"SortBy": "並び替え ",
"KeywordsPlaceholder": "キーワード(スペースで区切る)",
"FromNewToOld": "新しい順",
"FromOldToNew": "古い順",
"AttackPowerFromHighToLow": "攻撃力が高い順",
"AttackPowerFromLowToHigh": "攻撃力が低い順",
"DefensePowerFromHighToLow": "守備力が高い順",
"DefensePowerFromLowToHigh": "守備力が低い順",
"StarsRanksLevelsLinkFromHighToLow": "星/ランク/レベル/リンクが高い順",
"StarsRanksLevelsLinkFromLowToHigh": "星/ランク/レベル/リンクが低い順",
"PendulumScaleFromHighToLow": "ペンデュラムスケールが高い順",
"PendulumScaleFromLowToHigh": "ペンデュラムスケールが低い順",
"ResetSuccessful": "リセット成功",
"SaveSuccessful": "保存成功",
"NoDeckGroupFound": "対応するデッキグループが見つかりません",
"OnlyShowDecksIUploaded": "自分がアップロードしたデッキのみ表示",
"ShowAllOnlineDecks": "すべてのオンラインデッキを表示",
"CardDatabase": "カードデータベース",
"MDProOnlineDeck": "MDProオンラインデッキ"
},
"Filter": {
"CardFilter": "カードフィルター",
"Attribute": "属性",
"Race": "種族",
"Type": "種類",
"Level": "レベル",
"PendulumScale": "ペンデュラムスケール",
"Attack": "攻撃力",
"Defense": "守備力",
"Select": "-選択-",
"Minimum": "最小",
"Maximum": "最大",
"Confirm": "確認",
"Cancel": "キャンセル"
},
"CardDetails": {
"Level": "レベル",
"Type": "種類",
"Attribute": "属性",
"Race": "種族",
"Attack": "攻撃力",
"Defence": "守備力",
"PendulumScale": "ペンデュラムスケール",
"MonsterEffect": "モンスター効果",
"PendulumEffect": "ペンデュラム効果",
"CardEffect": "カード効果"
},
"WaitRoom": {
"Deck": "デッキ",
"JoinDuelist": "デュエリストとして参加",
"JoinSpectator": "観戦者として参加",
"DuelReady": "デュエル準備完了",
"CancelReady": "準備完了をキャンセル",
"LeaveRoom": "ルームを退出",
"Expand": "拡張",
"Collapse": "折りたたみ",
"Sidebar": "サイドバー",
"StartGame": "ゲームを開始",
"PlsRockPaperScissors": "じゃんけんをしてください",
"WaitOpponentPlayRockPaperScissors": "相手がじゃんけんをするのを待っています",
"PlsChooseWhoGoesFirst": "先行を選んでください",
"WaitingForGameToStart": "ゲーム開始を待っています",
"Scissors": "はさみ",
"Rock": "岩",
"Paper": "紙"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "プライベートルームを作成/参加",
"RoomPassword": "パスワード",
"Initial": "初期 ",
"InitialHandSize": "初期手札サイズ",
"DrawPerTurn": "1ターンごとのドロー",
"CardsAllowed": "許可されるカード",
"SimplifiedChinese": "簡体字中国語",
"CustomCards": "カスタムカード",
"ExclusiveCardsProhibited": "専用カード禁止",
"AllCards": "すべてのカード",
"DuelMode": "デュエルモード",
"SingleMatchMode": "シングルマッチモード",
"TournamentMode": "トーナメントモード",
"DuelRules": "デュエルルール",
"MasterRule1": "マスタールール1",
"MasterRule2": "マスタールール2",
"MasterRule3": "マスタールール3",
"NewMasterRule": "新マスタールール",
"MasterRule2020": "マスタールール2020",
"NoDeckCheck": "デッキチェックなし",
"NoShuffleDeck": "デッキシャッフルなし",
"40MinutesAutomaticOvertime": "40分自動延長",
"EnterYourFriendsPrivateRoomPassword": "友達のプライベートルームのパスワードをここに入力してください。",
"CreatePrivateRoom": "プライベートルームを作成",
"JoinPrivateRoom": "プライベートルームに参加"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "プレイヤーのユーザー名でルームを検索",
"RankedMatch": "ランクマッチ",
"Versus": "対"
},
"DeckSelect": {
"CopySuccessful": "コピー成功",
"CopyFailed.": "コピー失敗",
"CreateNewDeck": "新しいデッキを作成",
"ImportFromLocalFile": "ローカルファイルからインポート",
"ImportFromClipboard": "クリップボードからインポート",
"ClickOrDragFilesHereToUpload": "ここをクリックまたはファイルをドラッグしてアップロード",
"SupportsYdkExtension": "デッキファイルは.ydk拡張子のみサポートされています。",
"UnableToReadClipboardContent": "クリップボードの内容を読み取れません。"
},
"Chat": {
"PleaseEnterChatContent": "チャット内容を入力してください"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "カスタムルーム情報を入力してください",
"Server": "サーバー",
"KoishiServer": "コイシサーバー",
"UltraPreemptiveServer": "超先行サーバー",
"PlayerNickname": "プレイヤーニックネーム",
"RoomPasswordOptional": "ルームパスワード(任意)",
"JoinRoom": "ルームに参加"
},
"ReplayModal": {
"SelectReplay": "リプレイを選択",
"ClickOrDragFilesHereToUpload": "ここをクリックまたはファイルをドラッグしてアップロード",
"SupportsYrd3dExtension": ".yrp3d拡張子のリプレイファイルのみサポートされています。",
"StartReplay": "リプレイを開始",
"PleaseUploadReplayFile": "まずリプレイファイルをアップロードしてください。"
},
"Popover": {
"First": "先行",
"Second": "後攻"
},
"Menu": {
"DoYouSurrunder": "降参しますか?",
"Cancel": "キャンセル",
"Confirm": "確認",
"SelectPhase": "フェーズを選択してください",
"Deselect": "選択解除",
"SelectionComplete": "選択完了",
"ChatRoom": "チャットルーム"
},
"SelectCardModal": {
"PleaseSelect": "選択してください",
"Cards": "カード",
"SelectOneCardAtTime": "1回に1枚のカードを選択してください"
},
"SystemSettings": {
"AudioSettings": "オーディオ設定",
"TurnOnMusic": "音楽をオンにする",
"TurnOnSoundEffects": "効果音をオンにする",
"SwitchMusicAccordingToTheEnvironment": "環境に応じて音楽を切り替える",
"LanguageSettings": "言語"
} }
} }
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
"Match": "Match", "Match": "Match",
"DeckBuilding": "Montagem de baralho", "DeckBuilding": "Montagem de baralho",
"PersonalCenter": "Centro pessoal", "PersonalCenter": "Centro pessoal",
"MyCardCommunity": "Comunidade Mengka", "MyCardCommunity": "Comunidade MyCard",
"DuelDatabase": "Base de dados de duelos", "DuelDatabase": "Base de dados de duelos",
"LogOut": "Terminar sessão", "LogOut": "Terminar sessão",
"Login": "Iniciar sessão no Mengka", "Login": "Iniciar sessão no MyCard",
"Fullscreen": "Ecrã completo" "Fullscreen": "Ecrã completo",
"SystemSettings": "Configurações do sistema"
}, },
"Start": { "Start": {
"Title": "Plataforma de batalha online de Yu-Gi-Oh!", "Title": "Plataforma de batalha online de Yu-Gi-Oh!",
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
"MCCustomRoomTitle": "Sala Personalizada", "MCCustomRoomTitle": "Sala Personalizada",
"MCCustomRoomDesc": "Crie ou entre em salas personalizadas no servidor e batalhe com amigos.", "MCCustomRoomDesc": "Crie ou entre em salas personalizadas no servidor e batalhe com amigos.",
"MCSpectatorListTitle": "Lista de espectadores", "MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no Mengka MyCard.", "MCSpectatorListDesc": "Assista aos duelos atualmente em andamento no MyCard MyCard.",
"SinglePlayerModeTitle": "Modo de um jogador", "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.", "SinglePlayerModeDesc": "Inicia um duelo contra a IA no servidor Koishi 7210 para testar o teu deck ou apenas passar o tempo.",
"CustomRoomTitle": "Sala Personalizada", "CustomRoomTitle": "Sala Personalizada",
...@@ -36,5 +37,165 @@ ...@@ -36,5 +37,165 @@
"ReplayDesc": "Assiste livremente aos duelos passados e revive aqueles momentos emocionantes de reversão.", "ReplayDesc": "Assiste livremente aos duelos passados e revive aqueles momentos emocionantes de reversão.",
"WIPTitle": "Em desenvolvimento...", "WIPTitle": "Em desenvolvimento...",
"WIPDesc": "Aguarde por outras funcionalidades." "WIPDesc": "Aguarde por outras funcionalidades."
},
"BuildDeck": {
"EnterTheDeckName": "Nome do deck",
"Shuffle": "Embaralhar",
"Sort": "Ordenar",
"Clear": "Limpar",
"Reset": "Redefinir",
"Save": "Salvar",
"QuestionCircleTooltip": "Clique duas vezes para adicionar uma carta, clique com o botão direito para remover uma carta, pressione o botão do meio do mouse para alternar cartas entre o deck principal e o deck lateral.",
"Filter": "Filtrar",
"SortBy": "Ordenar por ",
"KeywordsPlaceholder": "Palavras-chave (separadas por espaços)",
"FromNewToOld": "Do novo para o antigo",
"FromOldToNew": "Do antigo para o novo",
"AttackPowerFromHighToLow": "Poder de ataque do alto para o baixo",
"AttackPowerFromLowToHigh": "Poder de ataque do baixo para o alto",
"DefensePowerFromHighToLow": "Poder de defesa do alto para o baixo",
"DefensePowerFromLowToHigh": "Poder de defesa do baixo para o alto",
"StarsRanksLevelsLinkFromHighToLow": "Estrelas/Rank/Níveis/Link do alto para o baixo",
"StarsRanksLevelsLinkFromLowToHigh": "Estrelas/Rank/Níveis/Link do baixo para o alto",
"PendulumScaleFromHighToLow": "Escala Pêndulo do alto para o baixo",
"PendulumScaleFromLowToHigh": "Escala Pêndulo do baixo para o alto",
"ResetSuccessful": "Redefinição bem-sucedida",
"SaveSuccessful": "Salvo com sucesso",
"NoDeckGroupFound": "Nenhum grupo de deck correspondente encontrado",
"OnlyShowDecksIUploaded": "Mostrar apenas decks que eu enviei",
"ShowAllOnlineDecks": "Mostrar todos os decks online",
"CardDatabase": "Banco de Dados de Cartas",
"MDProOnlineDeck": "Deck Online MDPro"
},
"Filter": {
"CardFilter": "Filtro de Carta",
"Attribute": "Atributo",
"Race": "Raça",
"Type": "Tipo",
"Level": "Nível",
"PendulumScale": "Escala Pêndulo",
"Attack": "Ataque",
"Defense": "Defesa",
"Select": "-Selecione-",
"Minimum": "Min",
"Maximum": "Máx",
"Confirm": "Confirmar",
"Cancel": "Cancelar"
},
"CardDetails": {
"Level": "Nível",
"Type": "Tipo",
"Attribute": "Atributo",
"Race": "Raça",
"Attack": "Ataque",
"Defence": "Defesa",
"PendulumScale": "Escala Pêndulo",
"MonsterEffect": "Efeito de Monstro",
"PendulumEffect": "Efeito Pêndulo",
"CardEffect": "Efeito da Carta"
},
"WaitRoom": {
"Deck": "Deck",
"JoinDuelist": "Juntar-se ao Duelista",
"JoinSpectator": "Juntar-se como Espectador",
"DuelReady": "Pronto para Duelo",
"CancelReady": "Cancelar Prontidão",
"LeaveRoom": "Sair da Sala",
"Expand": "Expandir",
"Collapse": "Recolher",
"Sidebar": "Barra Lateral",
"StartGame": "Iniciar Jogo",
"PlsRockPaperScissors": "Por favor, jogue Pedra, Papel, Tesoura",
"WaitOpponentPlayRockPaperScissors": "Aguarde o oponente jogar Pedra, Papel, Tesoura",
"PlsChooseWhoGoesFirst": "Por favor, escolha quem começa",
"WaitingForGameToStart": "Aguardando o início do jogo",
"Scissors": "Tesoura",
"Rock": "Pedra",
"Paper": "Papel"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "Criar/Entrar em Sala Privada",
"RoomPassword": "Senha",
"Initial": "Inicial ",
"InitialHandSize": "Tamanho da Mão Inicial",
"DrawPerTurn": "Compras por Turno",
"CardsAllowed": "Cartas Permitidas",
"SimplifiedChinese": "Chinês Simplificado",
"CustomCards": "Cartas Personalizadas",
"ExclusiveCardsProhibited": "Cartas Exclusivas Proibidas",
"AllCards": "Todas as Cartas",
"DuelMode": "Modo de Duelo",
"SingleMatchMode": "Modo de Partida Única",
"TournamentMode": "Modo de Torneio",
"DuelRules": "Regras de Duelo",
"MasterRule1": "Regra Mestre 1",
"MasterRule2": "Regra Mestre 2",
"MasterRule3": "Regra Mestre 3",
"NewMasterRule": "Nova Regra Mestre",
"MasterRule2020": "Regra Mestre 2020",
"NoDeckCheck": "Sem Verificação de Deck",
"NoShuffleDeck": "Sem Embaralhar Deck",
"40MinutesAutomaticOvertime": "40 Minutos de Tempo Extra Automático",
"EnterYourFriendsPrivateRoomPassword": "Insira a senha da sala privada do seu amigo aqui.",
"CreatePrivateRoom": "Criar Sala Privada",
"JoinPrivateRoom": "Entrar em Sala Privada"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "Procurar Sala pelo Nome do Jogador",
"RankedMatch": "Partida Ranqueada",
"Versus": "vs"
},
"DeckSelect": {
"CopySuccessful": "Cópia bem-sucedida",
"CopyFailed.": "Falha na cópia",
"CreateNewDeck": "Criar novo deck",
"ImportFromLocalFile": "Importar de arquivo local",
"ImportFromClipboard": "Importar da área de transferência",
"ClickOrDragFilesHereToUpload": "Clique ou arraste arquivos aqui para fazer upload",
"SupportsYdkExtension": "Suporta apenas arquivos de deck com a extensão .ydk.",
"UnableToReadClipboardContent": "Não foi possível ler o conteúdo da área de transferência."
},
"Chat": {
"PleaseEnterChatContent": "Por favor, insira o conteúdo do chat"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "Por favor, insira as informações da sala personalizada",
"Server": "Servidor",
"KoishiServer": "Servidor Koishi",
"UltraPreemptiveServer": "Servidor Ultra Preemptivo",
"PlayerNickname": "Apelido do Jogador",
"RoomPasswordOptional": "Senha da Sala (opcional)",
"JoinRoom": "Entrar na Sala"
},
"ReplayModal": {
"SelectReplay": "Selecionar Replay",
"ClickOrDragFilesHereToUpload": "Clique ou arraste arquivos aqui para fazer upload",
"SupportsYrd3dExtension": "Apenas arquivos de replay com a extensão .yrp3d são suportados.",
"StartReplay": "Iniciar Replay",
"PleaseUploadReplayFile": "Por favor, carregue o arquivo de replay primeiro."
},
"Popover": {
"First": "Primeiro",
"Second": "Segundo"
},
"Menu": {
"DoYouSurrunder": "Você se rende?",
"Cancel": "Cancelar",
"Confirm": "Confirmar",
"SelectPhase": "Por favor, selecione a fase",
"Deselect": "Deselecionar",
"SelectionComplete": "Seleção completa",
"ChatRoom": "Sala de Chat"
},
"SelectCardModal": {
"PleaseSelect": "Por favor, selecione",
"Cards": "Cartas",
"SelectOneCardAtTime": "Selecione uma carta por vez"
},
"SystemSettings": {
"AudioSettings": "Configurações de áudio",
"TurnOnMusic": "Música",
"TurnOnSoundEffects": "Efeitos sonoros",
"SwitchMusicAccordingToTheEnvironment": "Trocar música conforme o ambiente"
} }
} }
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
"Match": "Emparejamiento", "Match": "Emparejamiento",
"DeckBuilding": "Construcción de Mazo", "DeckBuilding": "Construcción de Mazo",
"PersonalCenter": "Centro personal", "PersonalCenter": "Centro personal",
"MyCardCommunity": "Comunidad Mengka", "MyCardCommunity": "Comunidad MyCard",
"DuelDatabase": "Base de datos de duelos", "DuelDatabase": "Base de datos de duelos",
"LogOut": "Cerrar sesión", "LogOut": "Cerrar sesión",
"Login": "Iniciar sesión en Mengka", "Login": "Iniciar sesión en MyCard",
"Fullscreen": "Pantalla completa" "Fullscreen": "Pantalla completa",
"SystemSettings": "Configuración del sistema"
}, },
"Start": { "Start": {
"Title": "Plataforma de batalla basada en la web de Yu-gi-oh", "Title": "Plataforma de batalla basada en la web de Yu-gi-oh",
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
"MCCustomRoomTitle": "Sala personalizada", "MCCustomRoomTitle": "Sala personalizada",
"MCCustomRoomDesc": "Crea o únete a salas personalizadas en el servidor y juega con amigos.", "MCCustomRoomDesc": "Crea o únete a salas personalizadas en el servidor y juega con amigos.",
"MCSpectatorListTitle": "Lista de espectadores", "MCSpectatorListTitle": "Lista de espectadores",
"MCSpectatorListDesc": "Observa los duelos que se están llevando a cabo en Mengka MyCard.", "MCSpectatorListDesc": "Observa los duelos que se están llevando a cabo en MyCard MyCard.",
"SinglePlayerModeTitle": "Modo un jugador", "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.", "SinglePlayerModeDesc": "Inicia un duelo contra la IA en el servidor Koishi 7210 para probar tu mazo o simplemente pasar el tiempo.",
"CustomRoomTitle": "Sala personalizada", "CustomRoomTitle": "Sala personalizada",
...@@ -36,5 +37,166 @@ ...@@ -36,5 +37,166 @@
"ReplayDesc": "Mira libremente duelos pasados y revive esos emocionantes momentos de reversión.", "ReplayDesc": "Mira libremente duelos pasados y revive esos emocionantes momentos de reversión.",
"WIPTitle": "En desarrollo...", "WIPTitle": "En desarrollo...",
"WIPDesc": "Espere por otras funciones." "WIPDesc": "Espere por otras funciones."
},
"BuildDeck": {
"EnterTheDeckName": "Nombre del mazo",
"Shuffle": "Barajar",
"Sort": "Ordenar",
"Clear": "Limpiar",
"Reset": "Reiniciar",
"Save": "Guardar",
"QuestionCircleTooltip": "Haz doble clic para agregar una carta, haz clic derecho para eliminar una carta, presiona la rueda del ratón para cambiar las cartas entre el mazo principal y el mazo lateral.",
"Filter": "Filtrar",
"SortBy": "Ordenar por ",
"KeywordsPlaceholder": "Palabras clave (separadas por espacios)",
"FromNewToOld": "De nuevo a viejo",
"FromOldToNew": "De viejo a nuevo",
"AttackPowerFromHighToLow": "Poder de ataque de mayor a menor",
"AttackPowerFromLowToHigh": "Poder de ataque de menor a mayor",
"DefensePowerFromHighToLow": "Poder de defensa de mayor a menor",
"DefensePowerFromLowToHigh": "Poder de defensa de menor a mayor",
"StarsRanksLevelsLinkFromHighToLow": "Estrellas/Rangos/Niveles/Enlace de mayor a menor",
"StarsRanksLevelsLinkFromLowToHigh": "Estrellas/Rangos/Niveles/Enlace de menor a mayor",
"PendulumScaleFromHighToLow": "Escala de péndulo de mayor a menor",
"PendulumScaleFromLowToHigh": "Escala de péndulo de menor a mayor",
"ResetSuccessful": "Reinicio exitoso",
"SaveSuccessful": "Guardado exitoso",
"NoDeckGroupFound": "No se encontró un grupo de mazo correspondiente",
"OnlyShowDecksIUploaded": "Mostrar solo los mazos que subí",
"ShowAllOnlineDecks": "Mostrar todos los mazos en línea",
"CardDatabase": "Base de datos de cartas",
"MDProOnlineDeck": "Mazo en línea MDPro"
},
"Filter": {
"CardFilter": "Filtro de carta",
"Attribute": "Atributo",
"Race": "Raza",
"Type": "Tipo",
"Level": "Nivel",
"PendulumScale": "Escala de péndulo",
"Attack": "Ataque",
"Defense": "Defensa",
"Select": "-Seleccionar-",
"Minimum": "Mín",
"Maximum": "Máx",
"Confirm": "Confirmar",
"Cancel": "Cancelar"
},
"CardDetails": {
"Level": "Nivel",
"Type": "Tipo",
"Attribute": "Atributo",
"Race": "Raza",
"Attack": "Ataque",
"Defence": "Defensa",
"PendulumScale": "Escala de péndulo",
"MonsterEffect": "Efecto de Monstruo",
"PendulumEffect": "Efecto de Péndulo",
"CardEffect": "Efecto de Carta"
},
"WaitRoom": {
"Deck": "Mazo",
"JoinDuelist": "Unirse como Duelista",
"JoinSpectator": "Unirse como Espectador",
"DuelReady": "Duelista Listo",
"CancelReady": "Cancelar Listo",
"LeaveRoom": "Salir de la Sala",
"Expand": "Expandir",
"Collapse": "Colapsar",
"Sidebar": "Barra Lateral",
"StartGame": "Iniciar Juego",
"PlsRockPaperScissors": "Por favor, juega Piedra, Papel o Tijeras",
"WaitOpponentPlayRockPaperScissors": "Esperando a que el oponente juegue Piedra, Papel o Tijeras",
"PlsChooseWhoGoesFirst": "Por favor, elige quién va primero",
"WaitingForGameToStart": "Esperando a que comience el juego",
"Scissors": "Tijeras",
"Rock": "Piedra",
"Paper": "Papel"
},
"CustomRoomContent": {
"CreateJoinPrivateRoom": "Crear/Unirse a una Sala Privada",
"RoomPassword": "Contraseña",
"Initial": "Inicial ",
"InitialHandSize": "Tamaño de Mano Inicial",
"DrawPerTurn": "Robo por Turno",
"CardsAllowed": "Cartas Permitidas",
"SimplifiedChinese": "Chino Simplificado",
"CustomCards": "Cartas Personalizadas",
"ExclusiveCardsProhibited": "Cartas Exclusivas Prohibidas",
"AllCards": "Todas las Cartas",
"DuelMode": "Modo de Duelo",
"SingleMatchMode": "Modo de Partida Única",
"TournamentMode": "Modo de Torneo",
"DuelRules": "Reglas de Duelo",
"MasterRule1": "Regla Maestra 1",
"MasterRule2": "Regla Maestra 2",
"MasterRule3": "Regla Maestra 3",
"NewMasterRule": "Nueva Regla Maestra",
"MasterRule2020": "Regla Maestra 2020",
"NoDeckCheck": "Sin Verificación de Mazo",
"NoShuffleDeck": "Sin Barajar Mazo",
"40MinutesAutomaticOvertime": "40 Minutos de Tiempo Extra Automático",
"EnterYourFriendsPrivateRoomPassword": "Ingresa la contraseña de la sala privada de tu amigo aquí.",
"CreatePrivateRoom": "Crear Sala Privada",
"JoinPrivateRoom": "Unirse a Sala Privada"
},
"WatchContent": {
"SearchRoomByPlayerUsername": "Buscar Sala por Nombre de Usuario del Jugador",
"RankedMatch": "Partida Clasificatoria",
"Versus": "vs"
},
"DeckSelect": {
"CopySuccessful": "Copia exitosa",
"CopyFailed.": "Copia fallida",
"CreateNewDeck": "Crear nuevo mazo",
"ImportFromLocalFile": "Importar desde archivo local",
"ImportFromClipboard": "Importar desde el portapapeles",
"ClickOrDragFilesHereToUpload": "Haz clic o arrastra archivos aquí para subir",
"SupportsYdkExtension": "Solo admite archivos de mazo con la extensión .ydk.",
"UnableToReadClipboardContent": "No se puede leer el contenido del portapapeles."
},
"Chat": {
"PleaseEnterChatContent": "Por favor, ingresa el contenido del chat"
},
"MatchModal": {
"PleaseEnterCustomRoomInformation": "Por favor, ingresa la información de la sala personalizada",
"Server": "Servidor",
"KoishiServer": "Servidor Koishi",
"UltraPreemptiveServer": "Servidor Ultra Preventivo",
"PlayerNickname": "Apodo del Jugador",
"RoomPasswordOptional": "Contraseña de la Sala (opcional)",
"JoinRoom": "Unirse a la Sala"
},
"ReplayModal": {
"SelectReplay": "Seleccionar Repetición",
"ClickOrDragFilesHereToUpload": "Haz clic o arrastra archivos aquí para subir",
"SupportsYrd3dExtension": "Solo se admiten archivos de repetición con la extensión .yrp3d.",
"StartReplay": "Iniciar Repetición",
"PleaseUploadReplayFile": "Por favor, sube primero el archivo de repetición."
},
"Popover": {
"First": "Primero",
"Second": "Segundo"
},
"Menu": {
"DoYouSurrunder": "¿Te rindes?",
"Cancel": "Cancelar",
"Confirm": "Confirmar",
"SelectPhase": "Por favor, selecciona la fase",
"Deselect": "Deseleccionar",
"SelectionComplete": "Selección Completa",
"ChatRoom": "Sala de Chat"
},
"SelectCardModal": {
"PleaseSelect": "Por favor, selecciona",
"Cards": "Cartas",
"SelectOneCardAtTime": "Selecciona una carta a la vez"
},
"SystemSettings": {
"AudioSettings": "Configuración de audio",
"TurnOnMusic": "Activar música",
"TurnOnSoundEffects": "Efectos de sonido",
"SwitchMusicAccordingToTheEnvironment": "Cambiar la música según el entorno",
"LanguageSettings": "Idioma"
} }
} }
...@@ -18,42 +18,140 @@ const resources = { ...@@ -18,42 +18,140 @@ const resources = {
Header: translationChinese.Header, Header: translationChinese.Header,
Start: translationChinese.Start, Start: translationChinese.Start,
Match: translationChinese.Match, Match: translationChinese.Match,
BuildDeck: translationChinese.BuildDeck,
Filter: translationChinese.Filter,
CardDetails: translationChinese.CardDetails,
WaitRoom: translationChinese.WaitRoom,
CustomRoomContent: translationChinese.CustomRoomContent,
WatchContent: translationChinese.WatchContent,
DeckSelect: translationChinese.DeckSelect,
Chat: translationChinese.Chat,
MatchModal: translationChinese.MatchModal,
ReplayModal: translationChinese.ReplayModal,
Popover: translationChinese.Popover,
Menu: translationChinese.Menu,
SelectCardModal: translationChinese.SelectCardModal,
SystemSettings: translationChinese.SystemSettings,
}, },
en: { en: {
Header: translationEnglish.Header, Header: translationEnglish.Header,
Start: translationEnglish.Start, Start: translationEnglish.Start,
Match: translationEnglish.Match, Match: translationEnglish.Match,
BuildDeck: translationEnglish.BuildDeck,
Filter: translationEnglish.Filter,
CardDetails: translationEnglish.CardDetails,
WaitRoom: translationEnglish.WaitRoom,
CustomRoomContent: translationEnglish.CustomRoomContent,
WatchContent: translationEnglish.WatchContent,
DeckSelect: translationEnglish.DeckSelect,
Chat: translationEnglish.Chat,
MatchModal: translationEnglish.MatchModal,
ReplayModal: translationEnglish.ReplayModal,
Popover: translationEnglish.Popover,
Menu: translationEnglish.Menu,
SelectCardModal: translationEnglish.SelectCardModal,
SystemSettings: translationEnglish.SystemSettings,
}, },
es: { es: {
Header: translationSpanish.Header, Header: translationSpanish.Header,
Start: translationSpanish.Start, Start: translationSpanish.Start,
Match: translationSpanish.Match, Match: translationSpanish.Match,
BuildDeck: translationSpanish.BuildDeck,
Filter: translationSpanish.Filter,
CardDetails: translationSpanish.CardDetails,
WaitRoom: translationSpanish.WaitRoom,
CustomRoomContent: translationSpanish.CustomRoomContent,
WatchContent: translationSpanish.WatchContent,
DeckSelect: translationSpanish.DeckSelect,
Chat: translationSpanish.Chat,
MatchModal: translationSpanish.MatchModal,
ReplayModal: translationSpanish.ReplayModal,
Popover: translationSpanish.Popover,
Menu: translationSpanish.Menu,
SelectCardModal: translationSpanish.SelectCardModal,
SystemSettings: translationSpanish.SystemSettings,
}, },
fr: { fr: {
Header: translationFrench.Header, Header: translationFrench.Header,
Start: translationFrench.Start, Start: translationFrench.Start,
Match: translationFrench.Match, Match: translationFrench.Match,
BuildDeck: translationFrench.BuildDeck,
Filter: translationFrench.Filter,
CardDetails: translationFrench.CardDetails,
WaitRoom: translationFrench.WaitRoom,
CustomRoomContent: translationFrench.CustomRoomContent,
WatchContent: translationFrench.WatchContent,
DeckSelect: translationFrench.DeckSelect,
Chat: translationFrench.Chat,
MatchModal: translationFrench.MatchModal,
ReplayModal: translationFrench.ReplayModal,
Popover: translationFrench.Popover,
Menu: translationFrench.Menu,
SelectCardModal: translationFrench.SelectCardModal,
SystemSettings: translationFrench.SystemSettings,
}, },
jp: { ja: {
Header: translationJapanese.Header, Header: translationJapanese.Header,
Start: translationJapanese.Start, Start: translationJapanese.Start,
Match: translationJapanese.Match, Match: translationJapanese.Match,
BuildDeck: translationJapanese.BuildDeck,
Filter: translationJapanese.Filter,
CardDetails: translationJapanese.CardDetails,
WaitRoom: translationJapanese.WaitRoom,
CustomRoomContent: translationJapanese.CustomRoomContent,
WatchContent: translationJapanese.WatchContent,
DeckSelect: translationJapanese.DeckSelect,
Chat: translationJapanese.Chat,
MatchModal: translationJapanese.MatchModal,
ReplayModal: translationJapanese.ReplayModal,
Popover: translationJapanese.Popover,
Menu: translationJapanese.Menu,
SelectCardModal: translationJapanese.SelectCardModal,
SystemSettings: translationJapanese.SystemSettings,
}, },
br: { br: {
Header: translationBrazilian.Header, Header: translationBrazilian.Header,
Start: translationBrazilian.Start, Start: translationBrazilian.Start,
Match: translationBrazilian.Match, Match: translationBrazilian.Match,
BuildDeck: translationBrazilian.BuildDeck,
Filter: translationBrazilian.Filter,
CardDetails: translationBrazilian.CardDetails,
WaitRoom: translationBrazilian.WaitRoom,
CustomRoomContent: translationBrazilian.CustomRoomContent,
WatchContent: translationBrazilian.WatchContent,
DeckSelect: translationBrazilian.DeckSelect,
Chat: translationBrazilian.Chat,
MatchModal: translationBrazilian.MatchModal,
ReplayModal: translationBrazilian.ReplayModal,
Popover: translationBrazilian.Popover,
Menu: translationBrazilian.Menu,
SelectCardModal: translationBrazilian.SelectCardModal,
SystemSettings: translationBrazilian.SystemSettings,
}, },
pt: { pt: {
Header: translationPortuguese.Header, Header: translationPortuguese.Header,
Start: translationPortuguese.Start, Start: translationPortuguese.Start,
Match: translationPortuguese.Match, Match: translationPortuguese.Match,
BuildDeck: translationPortuguese.BuildDeck,
Filter: translationPortuguese.Filter,
CardDetails: translationPortuguese.CardDetails,
WaitRoom: translationPortuguese.WaitRoom,
CustomRoomContent: translationPortuguese.CustomRoomContent,
WatchContent: translationPortuguese.WatchContent,
DeckSelect: translationPortuguese.DeckSelect,
Chat: translationPortuguese.Chat,
MatchModal: translationPortuguese.MatchModal,
ReplayModal: translationPortuguese.ReplayModal,
Popover: translationPortuguese.Popover,
Menu: translationPortuguese.Menu,
SelectCardModal: translationPortuguese.SelectCardModal,
SystemSettings: translationPortuguese.SystemSettings,
}, },
}; };
i18next.use(initReactI18next).init({ i18next.use(initReactI18next).init({
resources, resources,
lng: "cn", //default language lng: localStorage.getItem("language") ?? "cn", //default language
}); });
export default i18next; export default i18next;
import {
DatabaseFilled,
FullscreenOutlined,
LoginOutlined,
LogoutOutlined,
SettingFilled,
TeamOutlined,
UserOutlined,
} from "@ant-design/icons";
import { App, Avatar, Dropdown } from "antd"; import { App, Avatar, Dropdown } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
...@@ -19,7 +28,6 @@ import { ...@@ -19,7 +28,6 @@ import {
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { accountStore } from "@/stores"; import { accountStore } from "@/stores";
import { I18NSelector } from "../I18N";
import { Setting } from "../Setting"; import { Setting } from "../Setting";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { import {
...@@ -75,7 +83,6 @@ export const Component = () => { ...@@ -75,7 +83,6 @@ export const Component = () => {
const { pathname } = routerLocation; const { pathname } = routerLocation;
const pathnamesHideHeader = ["/waitroom", "/duel", "/side"]; const pathnamesHideHeader = ["/waitroom", "/duel", "/side"];
const { modal } = App.useApp(); const { modal } = App.useApp();
const callbackUrl = `${location.origin}/match/`; const callbackUrl = `${location.origin}/match/`;
const onLogin = () => location.replace(getSSOSignInUrl(callbackUrl)); const onLogin = () => location.replace(getSSOSignInUrl(callbackUrl));
const onLogout = () => { const onLogout = () => {
...@@ -109,7 +116,6 @@ export const Component = () => { ...@@ -109,7 +116,6 @@ export const Component = () => {
{i18n("DeckBuilding")} {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
...@@ -118,14 +124,20 @@ export const Component = () => { ...@@ -118,14 +124,20 @@ export const Component = () => {
{ {
label: ( label: (
<a href={NeosConfig.profileUrl} target="_blank"> <a href={NeosConfig.profileUrl} target="_blank">
{i18n("PersonalCenter")} <>
<UserOutlined style={{ fontSize: "16px" }} />{" "}
<strong>{i18n("PersonalCenter")}</strong>
</>
</a> </a>
), ),
}, },
{ {
label: ( label: (
<a href="https://ygobbs.com" target="_blank"> <a href="https://ygobbs.com" target="_blank">
{i18n("MyCardCommunity")} <>
<TeamOutlined style={{ fontSize: "16px" }} />{" "}
<strong>{i18n("MyCardCommunity")}</strong>
</>
</a> </a>
), ),
}, },
...@@ -135,15 +147,27 @@ export const Component = () => { ...@@ -135,15 +147,27 @@ export const Component = () => {
href="https://mycard.moe/ygopro/arena/#/" href="https://mycard.moe/ygopro/arena/#/"
target="_blank" target="_blank"
> >
{i18n("DuelDatabase")} <>
<DatabaseFilled style={{ fontSize: "16px" }} />{" "}
<strong>{i18n("DuelDatabase")}</strong>
</>
</a> </a>
), ),
}, },
{ {
label: "系统设置", label: (
<>
<SettingFilled />{" "}
<strong>{i18n("SystemSettings")}</strong>
</>
),
onClick: () => { onClick: () => {
modal.info({ modal.info({
content: <Setting />, content: (
<>
<Setting />
</>
),
centered: true, centered: true,
maskClosable: true, maskClosable: true,
icon: null, icon: null,
...@@ -152,13 +176,30 @@ export const Component = () => { ...@@ -152,13 +176,30 @@ export const Component = () => {
}, },
}, },
{ {
label: logined ? i18n("LogOut") : i18n("Login"), label: (
onClick: logined ? onLogout : onLogin, <>
<strong style={{ color: "#1890ff" }}>
<FullscreenOutlined style={{ fontSize: "16px" }} />{" "}
{i18n("Fullscreen")}
</strong>
</>
),
onClick: () => document.documentElement.requestFullscreen(),
}, },
{ {
label: i18n("Fullscreen"), label: logined ? (
onClick: () => document.documentElement.requestFullscreen(), <>
danger: true, <LogoutOutlined style={{ fontSize: "16px" }} />{" "}
<strong>{i18n("LogOut")}</strong>
</>
) : (
<>
<LoginOutlined style={{ fontSize: "16px" }} />{" "}
<strong>{i18n("Login")}</strong>
</>
),
onClick: logined ? onLogout : onLogin,
danger: logined ? true : false,
}, },
].map((x, key) => ({ ...x, key })), ].map((x, key) => ({ ...x, key })),
}} }}
......
...@@ -2,6 +2,7 @@ import { CopyOutlined, KeyOutlined } from "@ant-design/icons"; ...@@ -2,6 +2,7 @@ import { CopyOutlined, KeyOutlined } from "@ant-design/icons";
import type { CheckboxProps } from "antd"; import type { CheckboxProps } from "antd";
import { App, Button, Checkbox, Input } from "antd"; import { App, Button, Checkbox, Input } from "antd";
import React, { ChangeEvent, useEffect } from "react"; import React, { ChangeEvent, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { defaultOptions, getPrivateRoomID, Options } from "@/api"; import { defaultOptions, getPrivateRoomID, Options } from "@/api";
...@@ -80,13 +81,13 @@ export const CustomRoomContent: React.FC = () => { ...@@ -80,13 +81,13 @@ export const CustomRoomContent: React.FC = () => {
const onChangePrivateID = (event: ChangeEvent<HTMLInputElement>) => { const onChangePrivateID = (event: ChangeEvent<HTMLInputElement>) => {
mcCustomRoomStore.friendPrivateID = Number(event.target.value); mcCustomRoomStore.friendPrivateID = Number(event.target.value);
}; };
const { t: i18n } = useTranslation("CustomRoomContent");
return ( return (
<div className={styles.container}> <div className={styles.container}>
<p>创建/加入私密房间</p> <p>{i18n("CreateJoinPrivateRoom")}</p>
<div className={styles.clipboard}> <div className={styles.clipboard}>
<div className={styles.title}> <div className={styles.title}>
房间密码 {i18n("RoomPassword")}
<KeyOutlined /> <KeyOutlined />
</div> </div>
<Input <Input
...@@ -98,7 +99,7 @@ export const CustomRoomContent: React.FC = () => { ...@@ -98,7 +99,7 @@ export const CustomRoomContent: React.FC = () => {
<Button icon={<CopyOutlined />} onClick={onCopy} /> <Button icon={<CopyOutlined />} onClick={onCopy} />
</div> </div>
<div className={styles["digit-option"]}> <div className={styles["digit-option"]}>
<div className={styles.title}>初始LP</div> <div className={styles.title}>{i18n("Initial")}LP</div>
<Input <Input
className={styles.input} className={styles.input}
value={options.start_lp} value={options.start_lp}
...@@ -107,7 +108,7 @@ export const CustomRoomContent: React.FC = () => { ...@@ -107,7 +108,7 @@ export const CustomRoomContent: React.FC = () => {
/> />
</div> </div>
<div className={styles["digit-option"]}> <div className={styles["digit-option"]}>
<div className={styles.title}>初始手牌数</div> <div className={styles.title}>{i18n("InitialHandSize")}</div>
<Input <Input
className={styles.input} className={styles.input}
value={options.start_hand} value={options.start_hand}
...@@ -116,7 +117,7 @@ export const CustomRoomContent: React.FC = () => { ...@@ -116,7 +117,7 @@ export const CustomRoomContent: React.FC = () => {
/> />
</div> </div>
<div className={styles["digit-option"]}> <div className={styles["digit-option"]}>
<div className={styles.title}>每回合抽卡</div> <div className={styles.title}>{i18n("DrawPerTurn")}</div>
<Input <Input
className={styles.input} className={styles.input}
value={options.draw_count} value={options.draw_count}
...@@ -126,26 +127,26 @@ export const CustomRoomContent: React.FC = () => { ...@@ -126,26 +127,26 @@ export const CustomRoomContent: React.FC = () => {
</div> </div>
<div className={styles["select-option"]}> <div className={styles["select-option"]}>
<Select <Select
title="卡片允许" title={i18n("CardsAllowed")}
value={options.rule} value={options.rule}
options={[ options={[
{ value: 0, label: "OCG" }, { value: 0, label: "OCG" },
{ value: 1, label: "TCG" }, { value: 1, label: "TCG" },
{ value: 2, label: "简体中文" }, { value: 2, label: i18n("SimplifiedChinese") },
{ value: 3, label: "自制卡" }, { value: 3, label: i18n("CustomCards") },
{ value: 4, label: "专有卡禁止" }, { value: 4, label: i18n("ExclusiveCardsProhibited") },
{ value: 5, label: "所有卡片" }, { value: 5, label: i18n("AllCards") },
]} ]}
onChange={onChangeRule} onChange={onChangeRule}
/> />
</div> </div>
<div className={styles["select-option"]}> <div className={styles["select-option"]}>
<Select <Select
title="决斗模式" title={i18n("DuelMode")}
value={options.mode} value={options.mode}
options={[ options={[
{ value: 0, label: "单局模式" }, { value: 0, label: i18n("SingleMatchMode") },
{ value: 1, label: "比赛模式" }, { value: 1, label: i18n("TournamentMode") },
// {value: 2, label: "TAG"}, // {value: 2, label: "TAG"},
]} ]}
onChange={onChangeMode} onChange={onChangeMode}
...@@ -153,14 +154,14 @@ export const CustomRoomContent: React.FC = () => { ...@@ -153,14 +154,14 @@ export const CustomRoomContent: React.FC = () => {
</div> </div>
<div className={styles["select-option"]}> <div className={styles["select-option"]}>
<Select <Select
title="决斗规则" title={i18n("DuelRules")}
value={options.duel_rule} value={options.duel_rule}
options={[ options={[
{ value: 1, label: "大师规则1" }, { value: 1, label: i18n("MasterRule1") },
{ value: 2, label: "大师规则2" }, { value: 2, label: i18n("MasterRule2") },
{ value: 3, label: "大师规则3" }, { value: 3, label: i18n("MasterRule3") },
{ value: 4, label: "新大师规则" }, { value: 4, label: i18n("NewMasterRule") },
{ value: 5, label: "大师规则2020" }, { value: 5, label: i18n("MasterRule2020") },
]} ]}
onChange={onChangeDuelRule} onChange={onChangeDuelRule}
/> />
...@@ -170,26 +171,26 @@ export const CustomRoomContent: React.FC = () => { ...@@ -170,26 +171,26 @@ export const CustomRoomContent: React.FC = () => {
checked={options.no_check_deck} checked={options.no_check_deck}
onChange={onChangeNoCheckDeck} onChange={onChangeNoCheckDeck}
> >
不检查卡组 {i18n("NoDeckCheck")}
</Checkbox> </Checkbox>
<Checkbox <Checkbox
className={styles.check} className={styles.check}
checked={options.no_shuffle_deck} checked={options.no_shuffle_deck}
onChange={onChangeNoShuffleDeck} onChange={onChangeNoShuffleDeck}
> >
不切洗卡组 {i18n("NoShuffleDeck")}
</Checkbox> </Checkbox>
<Checkbox <Checkbox
className={styles.check} className={styles.check}
checked={options.auto_death} checked={options.auto_death}
onChange={onChangeAutoDeath} onChange={onChangeAutoDeath}
> >
40分自动加时 {i18n("40MinutesAutomaticOvertime")}
</Checkbox> </Checkbox>
<Input <Input
value={friendPrivateID} value={friendPrivateID}
onChange={onChangePrivateID} onChange={onChangePrivateID}
placeholder="在这输入你朋友的私密房间密码" placeholder={i18n("EnterYourFriendsPrivateRoomPassword")}
type="text" type="text"
/> />
</div> </div>
...@@ -200,13 +201,14 @@ export const CustomRoomFooter: React.FC<{ ...@@ -200,13 +201,14 @@ export const CustomRoomFooter: React.FC<{
onCreateRoom: () => void; onCreateRoom: () => void;
onJoinRoom: () => void; onJoinRoom: () => void;
}> = ({ onCreateRoom, onJoinRoom }) => { }> = ({ onCreateRoom, onJoinRoom }) => {
const { t: i18n } = useTranslation("CustomRoomContent");
return ( return (
<div className={styles.footer}> <div className={styles.footer}>
<Button className={styles.btn} onClick={onCreateRoom}> <Button className={styles.btn} onClick={onCreateRoom}>
创建私密房间 {i18n("CreatePrivateRoom")}
</Button> </Button>
<Button className={styles.btn} onClick={onJoinRoom}> <Button className={styles.btn} onClick={onJoinRoom}>
加入私密房间 {i18n("JoinPrivateRoom")}
</Button> </Button>
</div> </div>
); );
......
import { App, Button, Input, Modal } from "antd"; import { App, Button, Input, Modal } from "antd";
import React, { ChangeEvent, useEffect, useState } from "react"; import React, { ChangeEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
...@@ -41,6 +42,7 @@ export const MatchModal: React.FC = ({}) => { ...@@ -41,6 +42,7 @@ export const MatchModal: React.FC = ({}) => {
const [serverId, setServerId] = useState(0); const [serverId, setServerId] = useState(0);
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { t: i18n } = useTranslation("MatchModal");
const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => { const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => {
setPlayer(event.target.value); setPlayer(event.target.value);
...@@ -87,11 +89,11 @@ export const MatchModal: React.FC = ({}) => { ...@@ -87,11 +89,11 @@ export const MatchModal: React.FC = ({}) => {
return ( return (
<Modal <Modal
open={open} open={open}
title="请输入自定义房间信息" title={i18n("PleaseEnterCustomRoomInformation")}
onCancel={() => (matchStore.open = false)} onCancel={() => (matchStore.open = false)}
footer={ footer={
<Button onClick={handleSubmit} loading={confirmLoading}> <Button onClick={handleSubmit} loading={confirmLoading}>
加入房间 {i18n("JoinRoom")}
</Button> </Button>
} }
confirmLoading={confirmLoading} confirmLoading={confirmLoading}
...@@ -100,16 +102,16 @@ export const MatchModal: React.FC = ({}) => { ...@@ -100,16 +102,16 @@ export const MatchModal: React.FC = ({}) => {
<div className={styles["inputs-container"]}> <div className={styles["inputs-container"]}>
<Select <Select
className={styles.select} className={styles.select}
title="服务器" title={i18n("Server")}
value={serverId} value={serverId}
options={[ options={[
{ {
value: KOISHI_INDEX, value: KOISHI_INDEX,
label: "Koishi服", label: i18n("KoishiServer"),
}, },
{ {
value: PRERELEASE_INDEX, value: PRERELEASE_INDEX,
label: "超先行服", label: i18n("UltraPreemptiveServer"),
}, },
]} ]}
onChange={handleServerChange} onChange={handleServerChange}
...@@ -117,7 +119,7 @@ export const MatchModal: React.FC = ({}) => { ...@@ -117,7 +119,7 @@ export const MatchModal: React.FC = ({}) => {
<Input <Input
className={styles.input} className={styles.input}
type="text" type="text"
placeholder="玩家昵称" placeholder={i18n("PlayerNickname")}
value={player} value={player}
onChange={handlePlayerChange} onChange={handlePlayerChange}
required required
...@@ -126,7 +128,7 @@ export const MatchModal: React.FC = ({}) => { ...@@ -126,7 +128,7 @@ export const MatchModal: React.FC = ({}) => {
className={styles.input} className={styles.input}
type="text" type="text"
autoCorrect="off" autoCorrect="off"
placeholder="房间密码(可选)" placeholder={i18n("RoomPasswordOptional")}
value={passwd} value={passwd}
onChange={handlePasswdChange} onChange={handlePasswdChange}
/> />
......
import { Button, message, Modal, type UploadProps } from "antd"; import { Button, message, Modal, type UploadProps } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
...@@ -18,6 +19,7 @@ export const ReplayModal: React.FC = () => { ...@@ -18,6 +19,7 @@ export const ReplayModal: React.FC = () => {
const { open, hasStart } = useSnapshot(localStore); const { open, hasStart } = useSnapshot(localStore);
const [replay, setReplay] = useState<null | ArrayBuffer>(null); const [replay, setReplay] = useState<null | ArrayBuffer>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t: i18n } = useTranslation("ReplayModal");
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
name: "replay", name: "replay",
onChange(info) { onChange(info) {
...@@ -35,7 +37,7 @@ export const ReplayModal: React.FC = () => { ...@@ -35,7 +37,7 @@ export const ReplayModal: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const onSubmit = async () => { const onSubmit = async () => {
if (replay === null) { if (replay === null) {
message.error("请先上传录像文件"); message.error(`${i18n("PleaseUploadReplayFile")}`);
} else { } else {
setLoading(true); setLoading(true);
...@@ -72,22 +74,22 @@ export const ReplayModal: React.FC = () => { ...@@ -72,22 +74,22 @@ export const ReplayModal: React.FC = () => {
return ( return (
<Modal <Modal
title="选择回放" title={i18n("SelectReplay")}
open={open} open={open}
maskClosable={false} maskClosable={false}
confirmLoading={loading} confirmLoading={loading}
centered centered
footer={ footer={
<Button onClick={onSubmit} loading={loading}> <Button onClick={onSubmit} loading={loading}>
开始回放 {i18n("StartReplay")}
</Button> </Button>
} }
onCancel={() => (localStore.open = false)} onCancel={() => (localStore.open = false)}
> >
<Uploader <Uploader
{...uploadProps} {...uploadProps}
text="单击或拖动文件到此区域进行上传" text={i18n("ClickOrDragFilesHereToUpload")}
hint="仅支持后缀名为yrp3d的录像文件。" hint={i18n("SupportsYrd3dExtension")}
/> />
</Modal> </Modal>
); );
......
...@@ -2,6 +2,7 @@ import { SearchOutlined } from "@ant-design/icons"; ...@@ -2,6 +2,7 @@ import { SearchOutlined } from "@ant-design/icons";
import { App, Avatar, Button, Divider, Empty, Input } from "antd"; import { App, Avatar, Button, Divider, Empty, Input } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import useWebSocket, { ReadyState } from "react-use-websocket"; import useWebSocket, { ReadyState } from "react-use-websocket";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
...@@ -30,7 +31,7 @@ export const WatchContent: React.FC = () => { ...@@ -30,7 +31,7 @@ export const WatchContent: React.FC = () => {
// 暂时只支持竞技匹配的观战,TODO:后面需要加上娱乐匹配的支持 // 暂时只支持竞技匹配的观战,TODO:后面需要加上娱乐匹配的支持
const url = new URL(athleticWatchUrl); const url = new URL(athleticWatchUrl);
url.searchParams.set("filter", "started"); url.searchParams.set("filter", "started");
const { t: i18n } = useTranslation("WatchContent");
const { readyState } = useWebSocket(url.toString(), { const { readyState } = useWebSocket(url.toString(), {
onOpen: () => console.log("watch websocket opened."), onOpen: () => console.log("watch websocket opened."),
onClose: () => console.log("watch websocket closed."), onClose: () => console.log("watch websocket closed."),
...@@ -99,7 +100,7 @@ export const WatchContent: React.FC = () => { ...@@ -99,7 +100,7 @@ export const WatchContent: React.FC = () => {
<div className={styles.search}> <div className={styles.search}>
<Input <Input
className={styles.input} className={styles.input}
placeholder="通过玩家用户名搜索房间" placeholder={i18n("SearchRoomByPlayerUsername")}
bordered={false} bordered={false}
suffix={<Button type="text" icon={<SearchOutlined />} />} suffix={<Button type="text" icon={<SearchOutlined />} />}
value={query} value={query}
...@@ -129,10 +130,11 @@ export const WatchContent: React.FC = () => { ...@@ -129,10 +130,11 @@ export const WatchContent: React.FC = () => {
<Avatar src={room.users?.at(1)?.avatar} /> <Avatar src={room.users?.at(1)?.avatar} />
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{`${room.users?.at(0)?.username} 与 ${room.users?.at(1) {`${room.users?.at(0)?.username}` +
?.username} 的决斗`} ` ${i18n("Versus")} ` +
`${room.users?.at(1)?.username} 的决斗`}
</div> </div>
<div className={styles.mode}>竞技匹配</div> <div className={styles.mode}>{i18n("RankedMatch")}</div>
</div> </div>
<Divider className={styles.divider} /> <Divider className={styles.divider} />
</div> </div>
......
...@@ -159,7 +159,7 @@ export const Component: React.FC = () => { ...@@ -159,7 +159,7 @@ export const Component: React.FC = () => {
modal.info({ modal.info({
icon: null, icon: null,
width: "40vw", width: "40vw",
okText: "进入观战", okText: i18n("EnterSpectatorMode"),
onOk: async () => { onOk: async () => {
if (watchStore.watchID) { if (watchStore.watchID) {
setWatchLoading(true); setWatchLoading(true);
...@@ -184,7 +184,7 @@ export const Component: React.FC = () => { ...@@ -184,7 +184,7 @@ export const Component: React.FC = () => {
); );
} }
} else { } else {
message.error("请选择观战的房间"); message.error(`${i18n("PleaseSelectTheRoomToSpectate")}`);
} }
}, },
centered: true, centered: true,
......
import { Checkbox, Form, Slider, Space, Switch } from "antd"; import { Checkbox, Form, Slider, Space, Switch } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { settingStore } from "@/stores/settingStore"; import { settingStore } from "@/stores/settingStore";
export const AudioSetting: React.FC = () => { export const AudioSetting: React.FC = () => {
const { audio } = useSnapshot(settingStore); const { audio } = useSnapshot(settingStore);
const { t: i18n } = useTranslation("SystemSettings");
return ( return (
<Form <Form
initialValues={audio} initialValues={audio}
...@@ -15,7 +16,7 @@ export const AudioSetting: React.FC = () => { ...@@ -15,7 +16,7 @@ export const AudioSetting: React.FC = () => {
}} }}
labelAlign="left" labelAlign="left"
> >
<Form.Item label="开启音乐"> <Form.Item label={i18n("TurnOnMusic")}>
<Space size={16}> <Space size={16}>
<Form.Item name="enableMusic" noStyle valuePropName="checked"> <Form.Item name="enableMusic" noStyle valuePropName="checked">
<Checkbox /> <Checkbox />
...@@ -33,7 +34,7 @@ export const AudioSetting: React.FC = () => { ...@@ -33,7 +34,7 @@ export const AudioSetting: React.FC = () => {
</Form.Item> </Form.Item>
</Space> </Space>
</Form.Item> </Form.Item>
<Form.Item label="开启音效"> <Form.Item label={i18n("TurnOnSoundEffects")}>
<Space size={16}> <Space size={16}>
<Form.Item name="enableSoundEffects" noStyle valuePropName="checked"> <Form.Item name="enableSoundEffects" noStyle valuePropName="checked">
<Checkbox /> <Checkbox />
...@@ -53,7 +54,7 @@ export const AudioSetting: React.FC = () => { ...@@ -53,7 +54,7 @@ export const AudioSetting: React.FC = () => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="enableMusicSwitchByEnv" name="enableMusicSwitchByEnv"
label="根据环境切换音乐" label={i18n("SwitchMusicAccordingToTheEnvironment")}
valuePropName="checked" valuePropName="checked"
> >
<Switch /> <Switch />
......
import { AudioFilled, TranslationOutlined } from "@ant-design/icons";
import { ConfigProvider, Modal, Tabs, TabsProps } from "antd"; import { ConfigProvider, Modal, Tabs, TabsProps } from "antd";
import zhCN from "antd/locale/zh_CN"; import zhCN from "antd/locale/zh_CN";
import React from "react"; import React from "react";
import { render, unmountComponentAtNode } from "react-dom"; import { render, unmountComponentAtNode } from "react-dom";
import { useTranslation } from "react-i18next";
import { I18NSelector } from "../I18N";
import { theme } from "../theme"; import { theme } from "../theme";
import { AudioSetting } from "./Audio"; import { AudioSetting } from "./Audio";
...@@ -14,13 +17,27 @@ export interface SettingProps { ...@@ -14,13 +17,27 @@ export interface SettingProps {
export const Setting = (props: SettingProps) => { export const Setting = (props: SettingProps) => {
const { defaultKey = "audio" } = props; const { defaultKey = "audio" } = props;
const { t: i18n } = useTranslation("SystemSettings");
const items: TabsProps["items"] = [ const items: TabsProps["items"] = [
{ {
key: "audio", key: "audio",
label: "音频设置", label: (
<>
{i18n("AudioSettings")} <AudioFilled />
</>
),
children: <AudioSetting />, children: <AudioSetting />,
}, },
{
key: "language",
label: (
<>
{i18n("LanguageSettings")} <TranslationOutlined />
</>
),
children: <I18NSelector />,
},
]; ];
return <Tabs defaultActiveKey={defaultKey} items={items} />; return <Tabs defaultActiveKey={defaultKey} items={items} />;
......
import { Button, Input } from "antd"; import { Button, Input } from "antd";
import { useTranslation } from "react-i18next";
import { IconFont, ScrollableArea, useChat } from "@/ui/Shared"; import { IconFont, ScrollableArea, useChat } from "@/ui/Shared";
...@@ -12,7 +13,7 @@ interface ChatItem { ...@@ -12,7 +13,7 @@ interface ChatItem {
export const Chat: React.FC = () => { export const Chat: React.FC = () => {
const { dialogs, input, setInput, ref, onSend } = useChat(); const { dialogs, input, setInput, ref, onSend } = useChat();
const { t: i18n } = useTranslation("Chat");
return ( return (
<div className={styles.chat}> <div className={styles.chat}>
<ScrollableArea className={styles.dialogs} ref={ref}> <ScrollableArea className={styles.dialogs} ref={ref}>
...@@ -26,7 +27,7 @@ export const Chat: React.FC = () => { ...@@ -26,7 +27,7 @@ export const Chat: React.FC = () => {
value={input} value={input}
onChange={(event) => setInput(event.target.value)} onChange={(event) => setInput(event.target.value)}
autoSize autoSize
placeholder="请输入聊天内容" placeholder={i18n("PleaseEnterChatContent")}
onPressEnter={(e) => { onPressEnter={(e) => {
e.preventDefault(); e.preventDefault();
onSend(); onSend();
......
import { Button, Popover, Space } from "antd"; import { Button, Popover, Space } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { eventbus, Task } from "@/infra"; import { eventbus, Task } from "@/infra";
...@@ -35,10 +36,12 @@ export const MoraPopover: React.FC< ...@@ -35,10 +36,12 @@ export const MoraPopover: React.FC<
setOpen(false); setOpen(false);
}; };
const { t: i18n } = useTranslation("WaitRoom");
const map = { const map = {
[Mora.Rock]: "石头", [Mora.Rock]: i18n("Rock"),
[Mora.Scissors]: "剪刀", [Mora.Scissors]: i18n("Scissors"),
[Mora.Paper]: "", [Mora.Paper]: i18n("Paper"),
}; };
return ( return (
...@@ -73,7 +76,7 @@ export const TpPopover: React.FC< ...@@ -73,7 +76,7 @@ export const TpPopover: React.FC<
}> }>
> = ({ children, onSelect }) => { > = ({ children, onSelect }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { t: i18n } = useTranslation("WaitRoom");
// 需要在mora的service之中,emit一个事件,让这个组件监听到,然后打开popover // 需要在mora的service之中,emit一个事件,让这个组件监听到,然后打开popover
useEffect(() => { useEffect(() => {
eventbus.once(Task.Tp, () => { eventbus.once(Task.Tp, () => {
...@@ -88,8 +91,8 @@ export const TpPopover: React.FC< ...@@ -88,8 +91,8 @@ export const TpPopover: React.FC<
}; };
const map = { const map = {
[Tp.First]: "先手", [Tp.First]: i18n("First"),
[Tp.Second]: "后手", [Tp.Second]: i18n("Second"),
}; };
return ( return (
......
...@@ -17,6 +17,7 @@ import SelfType = ygopro.StocTypeChange.SelfType; ...@@ -17,6 +17,7 @@ import SelfType = ygopro.StocTypeChange.SelfType;
import { App, Avatar, Button, Skeleton, Space } from "antd"; import { App, Avatar, Button, Skeleton, Space } from "antd";
import classNames from "classnames"; import classNames from "classnames";
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";
...@@ -47,6 +48,7 @@ export const loader: LoaderFunction = async () => { ...@@ -47,6 +48,7 @@ export const loader: LoaderFunction = async () => {
}; };
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { t: i18n } = useTranslation("WaitRoom");
const { message } = App.useApp(); const { message } = App.useApp();
const { user } = useSnapshot(accountStore); const { user } = useSnapshot(accountStore);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
...@@ -142,8 +144,8 @@ export const Component: React.FC = () => { ...@@ -142,8 +144,8 @@ export const Component: React.FC = () => {
onClick={onReady} onClick={onReady}
> >
{me?.state === PlayerState.NO_READY {me?.state === PlayerState.NO_READY
? "决斗准备" ? i18n("DuelReady")
: "取消准备"} : i18n("CancelReady")}
</Button> </Button>
) : ( ) : (
<MoraAvatar <MoraAvatar
...@@ -261,12 +263,13 @@ const MoraAvatar: React.FC<{ mora?: Mora }> = ({ mora }) => ( ...@@ -261,12 +263,13 @@ const MoraAvatar: React.FC<{ mora?: Mora }> = ({ mora }) => (
const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
onDeckChange, onDeckChange,
}) => { }) => {
const { t: i18n } = useTranslation("WaitRoom");
const snapDeck = useSnapshot(deckStore); const snapDeck = useSnapshot(deckStore);
const snapRoom = useSnapshot(roomStore); const snapRoom = useSnapshot(roomStore);
return ( return (
<Space> <Space>
<Select <Select
title="卡组" title={i18n("Deck")}
showSearch showSearch
style={{ width: "15.6rem" }} style={{ width: "15.6rem" }}
defaultValue={snapDeck.decks[0].deckName} defaultValue={snapDeck.decks[0].deckName}
...@@ -290,7 +293,9 @@ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({ ...@@ -290,7 +293,9 @@ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
} }
}} }}
> >
{snapRoom.selfType === SelfType.OBSERVER ? "加入决斗者" : "加入观战"} {snapRoom.selfType === SelfType.OBSERVER
? i18n("JoinDuelist")
: i18n("JoinSpectator")}
{!!snapRoom.observerCount && ( {!!snapRoom.observerCount && (
<Avatar size="small" style={{ marginLeft: 8 }}> <Avatar size="small" style={{ marginLeft: 8 }}>
{snapRoom.observerCount} {snapRoom.observerCount}
...@@ -306,6 +311,7 @@ const SideButtons: React.FC<{ ...@@ -306,6 +311,7 @@ const SideButtons: React.FC<{
collapsed: boolean; collapsed: boolean;
}> = ({ switchCollapse, collapsed }) => { }> = ({ switchCollapse, collapsed }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { t: i18n } = useTranslation("WaitRoom");
return ( return (
<div className={styles["btns-side"]}> <div className={styles["btns-side"]}>
<Button <Button
...@@ -314,7 +320,9 @@ const SideButtons: React.FC<{ ...@@ -314,7 +320,9 @@ const SideButtons: React.FC<{
icon={ icon={
<span className={styles["btn-icon"]}> <span className={styles["btn-icon"]}>
<IconFont type="icon-exit" size={17} /> <IconFont type="icon-exit" size={17} />
<span className={styles["btn-text"]}>&#20;&#20;退出房间</span> <span className={styles["btn-text"]}>
&nbsp;&nbsp;{i18n("LeaveRoom")}
</span>
</span> </span>
} }
onClick={() => { onClick={() => {
...@@ -332,7 +340,8 @@ const SideButtons: React.FC<{ ...@@ -332,7 +340,8 @@ const SideButtons: React.FC<{
<span className={styles["btn-icon"]}> <span className={styles["btn-icon"]}>
<IconFont type="icon-side-bar-fill" size={16} /> <IconFont type="icon-side-bar-fill" size={16} />
<span className={styles["btn-text"]}> <span className={styles["btn-text"]}>
&#20;&#20;{collapsed ? "展开" : "收起"}侧栏 &nbsp;&nbsp;{collapsed ? i18n("Expand") : i18n("Collapse")}{" "}
{i18n("Sidebar")}
</span> </span>
</span> </span>
} }
...@@ -348,6 +357,7 @@ const ActionButton: React.FC<{ ...@@ -348,6 +357,7 @@ const ActionButton: React.FC<{
}> = ({ onMoraSelect, onTpSelect }) => { }> = ({ onMoraSelect, onTpSelect }) => {
const room = useSnapshot(roomStore); const room = useSnapshot(roomStore);
const { stage, isHost } = room; const { stage, isHost } = room;
const { t: i18n } = useTranslation("WaitRoom");
return ( return (
<MoraPopover onSelect={onMoraSelect}> <MoraPopover onSelect={onMoraSelect}>
<TpPopover onSelect={onTpSelect}> <TpPopover onSelect={onTpSelect}>
...@@ -367,32 +377,32 @@ const ActionButton: React.FC<{ ...@@ -367,32 +377,32 @@ const ActionButton: React.FC<{
{stage === RoomStage.WAITING ? ( {stage === RoomStage.WAITING ? (
<> <>
<IconFont type="icon-play" size={12} /> <IconFont type="icon-play" size={12} />
<span>开始游戏</span> <span>{i18n("StartGame")}</span>
</> </>
) : stage === RoomStage.HAND_SELECTING ? ( ) : stage === RoomStage.HAND_SELECTING ? (
<> <>
<IconFont type="icon-mora" size={20} /> <IconFont type="icon-mora" size={20} />
<span>请猜拳</span> <span>{i18n("PlsRockPaperScissors")}</span>
</> </>
) : stage === RoomStage.HAND_SELECTED ? ( ) : stage === RoomStage.HAND_SELECTED ? (
<> <>
<LoadingOutlined /> <LoadingOutlined />
<span>等待对方猜拳</span> <span>{i18n("WaitOpponentPlayRockPaperScissors")}</span>
</> </>
) : stage === RoomStage.TP_SELECTING ? ( ) : stage === RoomStage.TP_SELECTING ? (
<> <>
<IconFont type="icon-one" size={18} /> <IconFont type="icon-one" size={18} />
<span>请选择先后手</span> <span>{i18n("PlsChooseWhoGoesFirst")}</span>
</> </>
) : stage === RoomStage.TP_SELECTED ? ( ) : stage === RoomStage.TP_SELECTED ? (
<> <>
<LoadingOutlined /> <LoadingOutlined />
<span>等待游戏开始</span> <span>{i18n("WaitingForGameToStart")}</span>
</> </>
) : ( ) : (
<> <>
<LoadingOutlined /> <LoadingOutlined />
<span>等待游戏开始</span> <span>{i18n("WaitingForGameToStart")}</span>
</> </>
)} )}
</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