diff --git a/client/i18next-scanner.config.cjs b/client/i18next-scanner.config.cjs index aa0c6ad..dcc3df4 100644 --- a/client/i18next-scanner.config.cjs +++ b/client/i18next-scanner.config.cjs @@ -36,7 +36,7 @@ module.exports = { }, lngs: ['en','de'], ns: [], - defaultLng: 'en', + defaultLng: 'en-GB', defaultNs: 'translation', defaultValue: (lng, ns, key) => { if (lng === 'en') { diff --git a/client/public/locales/de/translation.json b/client/public/locales/de/translation.json index e7497d6..b201b0a 100644 --- a/client/public/locales/de/translation.json +++ b/client/public/locales/de/translation.json @@ -1,4 +1,5 @@ { + "Tactics": "Taktiken", "Lean Game Server": "Lean-Spieleserver", "

Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.

<1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp in a level, you can use it henceforth in any level.

The options are:

": "

Die Spielregeln bestimmen ob es erlaubt ist, Levels zu überspringen und ob das Spiel überprüft welche Taktiken und Theoreme freigeschaltet sind und nur diese im Beweis akzeptiert.

<1>Bemerkung: \"Freigeschaltete\" Taktiken (und Theoreme) werden durch zwei Faktoren bestimmt: The Menge der Taktiken die minimal notwending sind um den Level zu lösen und dazu die Menge aller Taktiken, die in einem anderen Level freigeschaltet wurden. Das bedeutet wenn <1>simp in einem Level freigeschaltet wird, kann diese Taktik danach in jeglichen Levels verwendet werden.", "Game Rules": "Spielregeln", @@ -7,5 +8,5 @@ "regular": "regulär", "relaxed": "relaxed", "none": "keine", - "Rules": "Regend" + "Rules": "Regeln" } diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index d4e1721..03053fa 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -1,4 +1,5 @@ { + "Tactics": "Tactics", "Lean Game Server": "Lean Game Server", "

Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.

<1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp in a level, you can use it henceforth in any level.

The options are:

": "

Game rules determine if it is allowed to skip levels and if the games runs checks to only allow unlocked tactics and theorems in proofs.

<1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp in a level, you can use it henceforth in any level.

The options are:

", "Game Rules": "Game Rules", diff --git a/client/src/app.tsx b/client/src/app.tsx index 8b597ce..42c5531 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -10,6 +10,7 @@ import './css/reset.css'; import './css/app.css'; import { PreferencesContext} from './components/infoview/context'; import UsePreferences from "./state/hooks/use_preferences" +import i18n from './i18n'; export const GameIdContext = React.createContext(undefined); @@ -18,12 +19,16 @@ function App() { const params = useParams() const gameId = "g/" + params.owner + "/" + params.repo - const {mobile, layout, isSavePreferences, setLayout, setIsSavePreferences} = UsePreferences() + const {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = UsePreferences() + + React.useEffect(() => { + i18n.changeLanguage(language) + }, [language]) return (
- + diff --git a/client/src/components/app_bar.tsx b/client/src/components/app_bar.tsx index bce6524..681f939 100644 --- a/client/src/components/app_bar.tsx +++ b/client/src/components/app_bar.tsx @@ -14,6 +14,7 @@ import { useAppDispatch, useAppSelector } from '../hooks' import { Button } from './button' import { downloadProgress } from './popup/erase' import ReactCountryFlag from "react-country-flag" +import { t } from 'i18next' /** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */ function MobileNavButtons({pageNumber, setPageNumber}: @@ -119,28 +120,46 @@ function InputModeButton({setNavOpen, isDropdown}) { } -/** button to toggle iimpressum popup */ +/** button to toggle iimpressum popup + * + * Note: Do not translate "Impressum"! +*/ function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) { - return } -/** button to toggle iimpressum popup */ -function LanguageButton({setNavOpen, toggleLangNav, isDropdown}) { - return +} + +function GameInfoButton({setNavOpen, toggleInfo}) { + return +} + +function EraseButton ({setNavOpen, toggleEraseMenu}) { + return +} + +function DownloadButton ({setNavOpen, gameId, gameProgress}) { + return +} + +function UploadButton ({setNavOpen, toggleUploadMenu}) { + return } @@ -153,6 +172,12 @@ function HomeButton({isDropdown}) { } +function LandingPageButton() { + return +} + /** button in mobile level to toggle inventory. * only displays a button if `setPageNumber` is set. */ @@ -184,9 +209,7 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres return
- +
@@ -197,33 +220,23 @@ export function WelcomeAppBar({pageNumber, setPageNumber, gameInfo, toggleImpres
- - - - - - + + + + + +
} /** the navigation bar in a level */ -export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber=undefined, setPageNumber=undefined} : { +export function LevelAppBar({isLoading, levelTitle, toggleImpressum, toggleInfo, togglePreferencesPopup, pageNumber=undefined, setPageNumber=undefined} : { isLoading: boolean, levelTitle: string, toggleImpressum: any, + toggleInfo: any, + togglePreferencesPopup: any, pageNumber?: number, setPageNumber?: any, }) { @@ -253,15 +266,16 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= + - {}} isDropdown={true} /> +
: <> {/* DESKTOP VERSION */}
- {worldTitle && `World: ${worldTitle}`} + {worldTitle && `${t("World")}: ${worldTitle}`}
{levelTitle} @@ -270,8 +284,12 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, pageNumber= - - {}} isDropdown={false} /> + +
+
+ + +
} diff --git a/client/src/components/infoview/context.ts b/client/src/components/infoview/context.ts index 6d51e19..e6dd6ee 100644 --- a/client/src/components/infoview/context.ts +++ b/client/src/components/infoview/context.ts @@ -79,15 +79,18 @@ export interface ProofStateProps { export interface IPreferencesContext extends PreferencesState{ mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout. setLayout: React.Dispatch>; - setIsSavePreferences: React.Dispatch>; + setIsSavePreferences: React.Dispatch>; + setLanguage: React.Dispatch>; } export const PreferencesContext = React.createContext({ mobile: false, layout: "auto", isSavePreferences: false, + language: "en", setLayout: () => {}, - setIsSavePreferences: () => {} + setIsSavePreferences: () => {}, + setLanguage: () => {}, }) export const WorldLevelIdContext = React.createContext<{ diff --git a/client/src/components/inventory.tsx b/client/src/components/inventory.tsx index 0622293..7441fff 100644 --- a/client/src/components/inventory.tsx +++ b/client/src/components/inventory.tsx @@ -10,6 +10,7 @@ import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadIn import { selectDifficulty, selectInventory } from '../state/progress'; import { store } from '../state/store'; import { useSelector } from 'react-redux'; +import { t } from 'i18next'; export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} : { @@ -24,7 +25,7 @@ export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=
{/* TODO: Click on Tactic: show info TODO: click on paste icon -> paste into command line */} -

Tactics

+

{t("Tactics")}

{levelInfo?.tactics && } diff --git a/client/src/components/level.tsx b/client/src/components/level.tsx index 1e193b5..25cc7c4 100644 --- a/client/src/components/level.tsx +++ b/client/src/components/level.tsx @@ -51,6 +51,8 @@ import { IConnectionProvider } from 'monaco-languageclient' import { monacoSetup } from 'lean4web/client/src/monacoSetup' import { onigasmH } from 'onigasm/lib/onigasmH' import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals' +import { InfoPopup } from './popup/game_info' +import { PreferencesPopup } from './popup/preferences' monacoSetup() @@ -60,17 +62,29 @@ function Level() { const levelId = parseInt(params.levelId) const worldId = params.worldId + const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext) + const gameId = React.useContext(GameIdContext) + const gameInfo = useGetGameInfoQuery({game: gameId}) + + // pop-ups const [impressum, setImpressum] = React.useState(false) + const [info, setInfo] = React.useState(false) + const [preferencesPopup, setPreferencesPopup] = React.useState(false) - const closeImpressum = () => { - setImpressum(false) - } + function closeImpressum() {setImpressum(false)} + function closeInfo() {setInfo(false)} + function closePreferencesPopup() {setPreferencesPopup(false)} + function toggleImpressum() {setImpressum(!impressum)} + function toggleInfo() {setInfo(!info)} + function togglePreferencesPopup() {setPreferencesPopup(!preferencesPopup)} return {levelId == 0 ? - : - } + : + } {impressum ? : null} + {info ? : null} + {preferencesPopup ? : null} } @@ -190,7 +204,7 @@ function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.MutableR
} -function PlayableLevel({impressum, setImpressum}) { +function PlayableLevel({impressum, setImpressum, toggleInfo, togglePreferencesPopup}) { const codeviewRef = useRef(null) const gameId = React.useContext(GameIdContext) const {worldId, levelId} = useContext(WorldLevelIdContext) @@ -396,7 +410,10 @@ function PlayableLevel({impressum, setImpressum}) { isLoading={level.isLoading} levelTitle={`${mobile ? '' : 'Level '}${levelId} / ${gameInfo.data?.worldSize[worldId]}` + (level?.data?.title && ` : ${level?.data?.title}`)} - toggleImpressum={toggleImpressum} /> + toggleImpressum={toggleImpressum} + toggleInfo={toggleInfo} + togglePreferencesPopup={togglePreferencesPopup} + /> {mobile? // TODO: This is copied from the `Split` component below... <> @@ -452,7 +469,7 @@ function IntroductionPanel({gameInfo}) { export default Level /** The site with the introduction text of a world */ -function Introduction({impressum, setImpressum}) { +function Introduction({impressum, setImpressum, toggleInfo, togglePreferencesPopup}) { const gameId = React.useContext(GameIdContext) const {mobile} = useContext(PreferencesContext) @@ -470,7 +487,7 @@ function Introduction({impressum, setImpressum}) { } return <> - + {gameInfo.isLoading ?
: mobile ? diff --git a/client/src/components/popup/language_config.json b/client/src/components/popup/language_config.json new file mode 100644 index 0000000..5e219dd --- /dev/null +++ b/client/src/components/popup/language_config.json @@ -0,0 +1,14 @@ +{ + "languages": [ + { + "iso": "en", + "flag": "GB", + "name": "English" + }, + { + "iso": "de", + "flag": "DE", + "name": "Deutsch" + } + ] +} diff --git a/client/src/components/popup/preferences.tsx b/client/src/components/popup/preferences.tsx index 47d1465..9b2d270 100644 --- a/client/src/components/popup/preferences.tsx +++ b/client/src/components/popup/preferences.tsx @@ -1,9 +1,10 @@ import * as React from 'react' -import { Input, MenuItem, Select, Typography } from '@mui/material' +import { Input, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material' import Markdown from '../markdown' import { Switch, Button, ButtonGroup } from '@mui/material'; import Box from '@mui/material/Box'; import Slider from '@mui/material/Slider'; +import supportedLanguages from './language_config.json' import FormControlLabel from '@mui/material/FormControlLabel'; @@ -11,102 +12,105 @@ import { IPreferencesContext } from "../infoview/context" import ReactCountryFlag from 'react-country-flag'; interface PreferencesPopupProps extends Omit { - handleClose: () => void + handleClose: () => void } -export function PreferencesPopup({ layout, setLayout, isSavePreferences, setIsSavePreferences, handleClose }: PreferencesPopupProps) { +export function PreferencesPopup({ layout, setLayout, isSavePreferences, language, setIsSavePreferences, handleClose, setLanguage }: PreferencesPopupProps) { - const marks = [ - { - value: 0, - label: 'Mobile', - key: "mobile" - }, - { - value: 1, - label: 'Auto', - key: "auto" - }, - { - value: 2, - label: 'Desktop', - key: "desktop" - }, - ]; + const marks = [ + { + value: 0, + label: 'Mobile', + key: "mobile" + }, + { + value: 1, + label: 'Auto', + key: "auto" + }, + { + value: 2, + label: 'Desktop', + key: "desktop" + }, + ]; - const handlerChangeLayout = (_: Event, value: number) => { - setLayout(marks[value].key as IPreferencesContext["layout"]) - } + const handlerChangeLayout = (_: Event, value: number) => { + setLayout(marks[value].key as IPreferencesContext["layout"]) + } - return
-
-
-
- -
-
-

Language

-
-
- - - - } - label="" - /> -
-
-
-
-

Layout

-
-
- - item.key === layout).value} - step={1} - marks={marks} - max={2} - sx={{ - '& .MuiSlider-track': { display: 'none', }, - }} - onChange={handlerChangeLayout} - /> - - } - label="" - /> -
-
+ const handlerChangeLanguage = (ev: SelectChangeEvent) => { + setLanguage(ev.target.value as IPreferencesContext["language"]) + } -
-
- setIsSavePreferences(!isSavePreferences)} - name="checked" - color="primary" - /> - } - label="Save my settings (in the browser store)" - labelPlacement="end" - /> -
-
-
+ return
+
+
+
+ +
+
+

Language

+
+
+ + + + } + label="" + /> +
+
+
+

Layout

+
+
+ + item.key === layout).value} + step={1} + marks={marks} + max={2} + sx={{ + '& .MuiSlider-track': { display: 'none', }, + }} + onChange={handlerChangeLayout} + /> + + } + label="" + /> +
+
+ +
+
+ setIsSavePreferences(!isSavePreferences)} + name="checked" + color="primary" + /> + } + label="Save my settings (in the browser store)" + labelPlacement="end" + /> +
+
+
+
} diff --git a/client/src/components/welcome.tsx b/client/src/components/welcome.tsx index a7c1697..b2a8d17 100644 --- a/client/src/components/welcome.tsx +++ b/client/src/components/welcome.tsx @@ -65,7 +65,7 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string, function Welcome() { const gameId = React.useContext(GameIdContext) const {mobile} = React.useContext(PreferencesContext) - const {layout, isSavePreferences, setLayout, setIsSavePreferences} = React.useContext(PreferencesContext) + const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext) const gameInfo = useGetGameInfoQuery({game: gameId}) const inventory = useLoadInventoryOverviewQuery({game: gameId}) @@ -135,7 +135,7 @@ function Welcome() { {eraseMenu? : null} {uploadMenu? : null} {info ? : null} - {preferencesPopup ? : null} + {preferencesPopup ? : null} } diff --git a/client/src/state/hooks/use_preferences.ts b/client/src/state/hooks/use_preferences.ts index 3b4f66c..fcdef76 100644 --- a/client/src/state/hooks/use_preferences.ts +++ b/client/src/state/hooks/use_preferences.ts @@ -1,9 +1,10 @@ import React, { useState } from "react"; import { useAppDispatch, useAppSelector } from "../../hooks"; -import { - PreferencesState, - setLayout as setPreferencesLayout, +import { + PreferencesState, + setLayout as setPreferencesLayout, setIsSavePreferences as setPreferencesIsSavePreferences, + setLanguage as setLanguagePreferences, getWindowDimensions, AUTO_SWITCH_THRESHOLD } from "../preferences"; @@ -19,6 +20,9 @@ const UsePreferences = () => { const isSavePreferences = useAppSelector((state) => state.preferences.isSavePreferences); const setIsSavePreferences = (isSave: boolean) => dispatch(setPreferencesIsSavePreferences(isSave)) + const language = useAppSelector((state) => state.preferences.language); + const setLanguage = (lang: string) => dispatch(setLanguagePreferences(lang)) + const automaticallyAdjustLayout = () => { const {width} = getWindowDimensions() setMobile(width < AUTO_SWITCH_THRESHOLD) @@ -28,14 +32,14 @@ const UsePreferences = () => { if (layout === "auto"){ void automaticallyAdjustLayout() window.addEventListener('resize', automaticallyAdjustLayout) - + return () => window.removeEventListener('resize', automaticallyAdjustLayout) } else { setMobile(layout === "mobile") } }, [layout]) - return {mobile, layout, isSavePreferences, setLayout, setIsSavePreferences} -} + return {mobile, layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} +} -export default UsePreferences; \ No newline at end of file +export default UsePreferences; diff --git a/client/src/state/preferences.ts b/client/src/state/preferences.ts index 7ccbe30..327af68 100644 --- a/client/src/state/preferences.ts +++ b/client/src/state/preferences.ts @@ -5,6 +5,7 @@ import { loadPreferences, removePreferences, savePreferences } from "./local_sto export interface PreferencesState { layout: "mobile" | "auto" | "desktop"; isSavePreferences: boolean; + language: string; } export function getWindowDimensions() { @@ -16,7 +17,8 @@ export const AUTO_SWITCH_THRESHOLD = 800 const initialState: PreferencesState = loadPreferences() ??{ layout: "auto", - isSavePreferences: false + isSavePreferences: false, + language: "en", } export const preferencesSlice = createSlice({ @@ -29,7 +31,10 @@ export const preferencesSlice = createSlice({ setIsSavePreferences: (state, action) => { state.isSavePreferences = action.payload; }, + setLanguage: (state, action) => { + state.language = action.payload; + }, }, }); -export const { setLayout, setIsSavePreferences } = preferencesSlice.actions; +export const { setLayout, setIsSavePreferences, setLanguage } = preferencesSlice.actions;