diff --git a/package.json b/package.json index fbb361d..1824a26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "periotrisjs", - "version": "2.4.0", + "version": "2.4.1", "description": "Get familiar with the Periodic Table of Elements in a fun way, directly in your browsers.", "scripts": { "clean": "yarn run -B rimraf build coverage public && yarn run -B gatsby clean", diff --git a/src/common/TResult.ts b/src/common/TResult.ts new file mode 100644 index 0000000..5155bea --- /dev/null +++ b/src/common/TResult.ts @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021-present Rong "Mantle" Bao + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + */ + +export type TResult = { ok: true; value?: T } | { ok: false; err?: E } diff --git a/src/common/index.ts b/src/common/index.ts index 900c5f0..812b983 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -33,12 +33,14 @@ import { import { flushed } from "./flushed" import { formatDuration } from "./formatDuration" import { isNil } from "./isNil" +import { getLocalizedLangName } from "./localize" import { queryPath } from "./queryPath" import { rearrange } from "./rearrange" import { waitForEvent } from "./waitForEvent" import type { ISize } from "./ISize" import type { TPosition } from "./TPosition" +import type { TResult } from "./TResult" export { AutoplaySentinel, @@ -56,9 +58,10 @@ export { StopwatchUpdateIntervalMilliseconds, flushed, formatDuration, + getLocalizedLangName, isNil, queryPath, rearrange, waitForEvent, } -export type { ISize, TPosition } +export type { ISize, TPosition, TResult } diff --git a/src/common/localize.ts b/src/common/localize.ts new file mode 100644 index 0000000..c00cbcd --- /dev/null +++ b/src/common/localize.ts @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021-present Rong "Mantle" Bao + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/ . + */ + +export function getLocalizedLangName(lang: string): string { + switch (lang) { + case "zh": + return "中文" + case "en": + return "English" + + default: + throw new RangeError(`getLocalizedLangName: unknown language ${lang}`) + } +} diff --git a/src/components/FileFormControl.tsx b/src/components/FileFormControl.tsx index 8c643ca..faf84f8 100644 --- a/src/components/FileFormControl.tsx +++ b/src/components/FileFormControl.tsx @@ -26,7 +26,7 @@ import TextField from "@mui/material/TextField" import Tooltip from "@mui/material/Tooltip" import styled from "@mui/system/styled" -import { isNil } from "../common" +import type { TResult } from "../common" const HiddenInput = styled("input")({ display: "none", @@ -79,13 +79,14 @@ interface IFileFormControlProps { readonly helperText: string readonly readOnly?: boolean readonly disabled?: boolean - readonly onFileChange: (newContent: string) => boolean | void + readonly onFileChange: (newContent: string) => TResult readonly contentPreprocessor?: (content: string) => string } export const FileFormControl = (props: IFileFormControlProps) => { const [fileContent, setFileContent] = React.useState(props.initialFileContent) const [error, setError] = React.useState(false) + const [helperText, setHelperText] = React.useState(props.helperText) return ( @@ -95,7 +96,7 @@ export const FileFormControl = (props: IFileFormControlProps) => { fullWidth value={fileContent} label={props.label} - helperText={props.helperText} + helperText={helperText} disabled={props.disabled ?? false} error={error} InputProps={{ @@ -113,9 +114,11 @@ export const FileFormControl = (props: IFileFormControlProps) => { let content = (await head(event.target.files)?.text()) ?? "" content = props.contentPreprocessor?.(content) ?? content const result = props.onFileChange(content) - const isSuccessful = isNil(result) || result setFileContent(content) - setError(!isSuccessful) + setError(!result.ok) + setHelperText( + result.ok ? props.helperText : result.err ?? props.helperText + ) }} /> diff --git a/src/components/NumberFormControl.tsx b/src/components/NumberFormControl.tsx index 25aa4d9..36f3067 100644 --- a/src/components/NumberFormControl.tsx +++ b/src/components/NumberFormControl.tsx @@ -21,6 +21,8 @@ import TextField from "@mui/material/TextField" import { isNil } from "../common" +import type { TResult } from "../common" + interface INumberFormControlProps { readonly id: string readonly initialContent: string @@ -34,13 +36,14 @@ interface INumberFormControlProps { readonly min?: number readonly max?: number readonly disabled?: boolean - readonly onChange: (newContent: string) => boolean | void + readonly onChange: (newContent: string) => TResult readonly contentPreprocessor?: (content: string) => string } export const NumberFormControl = (props: INumberFormControlProps) => { const [content, setContent] = React.useState(props.initialContent) const [error, setError] = React.useState(false) + const [helperText, setHelperText] = React.useState(props.helperText) return ( { value={content} label={props.label} type="number" - helperText={props.helperText} + helperText={helperText} InputProps={{ ...props.adornments }} inputProps={{ + inputMode: "numeric", step: props.step, min: props.min, max: props.max, @@ -62,9 +66,11 @@ export const NumberFormControl = (props: INumberFormControlProps) => { ? event.target.value : props.contentPreprocessor(event.target.value) const result = props.onChange(content) - const isSuccessful = isNil(result) || result setContent(content) - setError(!isSuccessful) + setError(!result.ok) + setHelperText( + result.ok ? props.helperText : result.err ?? props.helperText + ) }} /> ) diff --git a/src/components/gameControlBackdrop/GameControlBackdrop.tsx b/src/components/gameControlBackdrop/GameControlBackdrop.tsx index c489148..091c08f 100644 --- a/src/components/gameControlBackdrop/GameControlBackdrop.tsx +++ b/src/components/gameControlBackdrop/GameControlBackdrop.tsx @@ -17,6 +17,7 @@ import { navigate } from "gatsby" import React from "react" +import { useI18next } from "gatsby-plugin-react-i18next" import Backdrop from "@mui/material/Backdrop" import Button from "@mui/material/Button" @@ -33,13 +34,15 @@ interface IGameNotStartedContentProps { } const GameNotStartedContent = (props: IGameNotStartedContentProps) => { + const { t } = useI18next() + return ( <> - - Welcome! Your task: complete the Periodic Table. + + {t("typ_h_not_started_intro")} - - A/D/S/Swipe: move by one. W/Tap: rotate. Space/Long press: drop. + + {t("typ_p_not_started_intro")} @@ -64,13 +67,15 @@ const GameNotStartedContent = (props: IGameNotStartedContentProps) => { } const GamePreparingContent = () => { + const { t } = useI18next() + return ( <> - - Good luck! + + {t("typ_h_preparing")} - - Generating new map for you. The game will start in a few seconds. + + {t("typ_p_preparing")} @@ -83,13 +88,15 @@ interface IGameLostContentProps { } const GameLostContent = (props: IGameLostContentProps) => { + const { t } = useI18next() + return ( <> - - Oops... + + {t("typ_h_lost")} - - This does not seem to be right. Ready to give it another shot? + + {t("typ_p_lost")} @@ -119,13 +126,15 @@ interface IGameWonContentProps { } const GameWonContent = (props: IGameWonContentProps) => { + const { t } = useI18next() + return ( <> - - Congrats! + + {t("typ_h_won")} - - You finished the game! Don't hesitate to brag about it. + + {t("typ_p_won")} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index c645543..a54ef2e 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -15,6 +15,6 @@ * along with this program. If not, see https://www.gnu.org/licenses/ . */ -import { langs, defaultLang } from "./lang" +import { defaultLang, langs } from "./lang" -export { langs, defaultLang } +export { defaultLang, langs } diff --git a/src/i18n/locales/en/autoplay.json b/src/i18n/locales/en/autoplay.json new file mode 100644 index 0000000..a8c7ff6 --- /dev/null +++ b/src/i18n/locales/en/autoplay.json @@ -0,0 +1,13 @@ +{ + "typ_h_not_started_intro": "Welcome to Periotris.js AUTOPLAY", + "typ_p_not_started_intro": "In AUTOPLAY, the system takes over game input, and all user key presses and gestures are ignored. This session will not be recorded in play history. Apart from those, it is identical to Normal Mode and observes the same user settings, including map, color scheme and game speed.", + "cap_home": "HOME", + "cap_start": "START", + "cap_retry": "RETRY", + "typ_h_preparing": "Please wait", + "typ_p_preparing": "Generating new unique patterns. The demonstration will start in a few seconds.", + "typ_h_lost": "Ouch...", + "typ_p_lost": "This should not happen. Please report this to the developer.", + "typ_h_won": "Demo complete", + "typ_p_won": "This AUTOPLAY session is over. Feel free to start a new one." +} diff --git a/src/i18n/locales/en/game.json b/src/i18n/locales/en/game.json new file mode 100644 index 0000000..3c41114 --- /dev/null +++ b/src/i18n/locales/en/game.json @@ -0,0 +1,13 @@ +{ + "typ_h_not_started_intro": "Welcome to Periotris.js! Your task: complete the Periodic Table", + "typ_p_not_started_intro": "A/D/S/Swipe: move by one; W/Tap: rotate; Space/Long press: drop", + "cap_home": "HOME", + "cap_start": "START", + "cap_retry": "RETRY", + "typ_h_preparing": "Good luck!", + "typ_p_preparing": "Generating new unique patterns for you. The game will start in a few seconds.", + "typ_h_lost": "Oops...", + "typ_p_lost": "This does not seem to be right. Ready to give it another shot?", + "typ_h_won": "Congrats!", + "typ_p_won": "You finished the game! Don't hesitate to brag about it." +} diff --git a/src/i18n/locales/zh/autoplay.json b/src/i18n/locales/zh/autoplay.json new file mode 100644 index 0000000..3736211 --- /dev/null +++ b/src/i18n/locales/zh/autoplay.json @@ -0,0 +1,13 @@ +{ + "typ_h_not_started_intro": "欢迎来到 Periotris.js 自动演示模式", + "typ_p_not_started_intro": "在自动演示模式中,系统将接管游戏输入,用户发出的按键与触屏手势无效。本次游玩将不计入历史。除了以上几点,自动演示模式与常规模式相同,并采用相同的用户自定义设置,包括地图、色彩主题和游戏速度。", + "cap_home": "首页", + "cap_start": "开始", + "cap_retry": "重试", + "typ_h_preparing": "请稍候", + "typ_p_preparing": "正在生成新的分割排列。自动演示将马上开始。", + "typ_h_lost": "不好……", + "typ_p_lost": "这不应该发生。请向开发者报告该事件。", + "typ_h_won": "演示完成", + "typ_p_won": "本次自动演示已结束。你可以开始一次新的自动演示。" +} diff --git a/src/i18n/locales/zh/game.json b/src/i18n/locales/zh/game.json new file mode 100644 index 0000000..56c0665 --- /dev/null +++ b/src/i18n/locales/zh/game.json @@ -0,0 +1,13 @@ +{ + "typ_h_not_started_intro": "欢迎来到 Periotris.js !你的任务:拼出化学元素周期表", + "typ_p_not_started_intro": "A/D/S/触屏滑动:移动一格;W/触屏短按:旋转;Space/触屏长按:立即下落", + "cap_home": "首页", + "cap_start": "开始", + "cap_retry": "重试", + "typ_h_preparing": "祝你好运!", + "typ_p_preparing": "正在生成全新的分割排列。游戏将马上开始。", + "typ_h_lost": "糟糕……", + "typ_p_lost": "这种排列方式不正确。再试一次?", + "typ_h_won": "祝贺!", + "typ_p_won": "你成功完成了这次挑战,请尽情欣赏你的杰作吧!" +} diff --git a/src/pages/autoplay.tsx b/src/pages/autoplay.tsx index 376d76c..b96bb89 100644 --- a/src/pages/autoplay.tsx +++ b/src/pages/autoplay.tsx @@ -89,7 +89,7 @@ export const query = graphql` } locales: allLocale( - filter: { ns: { in: ["index"] }, language: { eq: $language } } + filter: { ns: { in: ["autoplay"] }, language: { eq: $language } } ) { edges { node { diff --git a/src/pages/game.tsx b/src/pages/game.tsx index 7790f27..00c4a61 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -127,7 +127,7 @@ export const query = graphql` } locales: allLocale( - filter: { ns: { in: ["index"] }, language: { eq: $language } } + filter: { ns: { in: ["game"] }, language: { eq: $language } } ) { edges { node { diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 005602d..55a9074 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -73,7 +73,7 @@ const App = ({ data }: PageProps) => { - +