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>simp1> in a level, you can use it henceforth in any level.1>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>simp1> 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>simp1> in a level, you can use it henceforth in any level.1>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>simp1> in a level, you can use it henceforth in any level.1>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 {toggleImpressum(ev); setNavOpen(false)}}>
- {isDropdown && <> Info & Impressum>}
+ {isDropdown && <> Impressum>}
}
-/** button to toggle iimpressum popup */
-function LanguageButton({setNavOpen, toggleLangNav, isDropdown}) {
- return {toggleLangNav(ev); setNavOpen(false)}}>
-
- {isDropdown && <> Language>}
+function PreferencesButton({setNavOpen, togglePreferencesPopup}) {
+ return {togglePreferencesPopup(); setNavOpen(false)}}>
+ {t("Preferences")}
+
+}
+
+function GameInfoButton({setNavOpen, toggleInfo}) {
+ return {toggleInfo(); setNavOpen(false)}}>
+ Game Info
+
+}
+
+function EraseButton ({setNavOpen, toggleEraseMenu}) {
+ return {toggleEraseMenu(); setNavOpen(false)}}>
+ Erase
+
+}
+
+function DownloadButton ({setNavOpen, gameId, gameProgress}) {
+ return {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
+ Download
+
+}
+
+function UploadButton ({setNavOpen, toggleUploadMenu}) {
+ return {toggleUploadMenu(); setNavOpen(false)}}>
+ Upload
}
@@ -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
-
{toggleInfo(); setNavOpen(false)}}>
- Game Info
-
-
{toggleEraseMenu(); setNavOpen(false)}}>
- Erase
-
-
{downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
- Download
-
-
{toggleUploadMenu(); setNavOpen(false)}}>
- Upload
-
-
{toggleImpressum(); setNavOpen(false)}}>
- Impressum
-
-
{togglePreferencesPopup(); setNavOpen(false)}}>
- Preferences
-
+
+
+
+
+
+
}
/** 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
-
-
-
-
-
- English
-
-
-
- }
- 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
+
+
+
+
+ {supportedLanguages.languages.map(lang => {return {lang.name} })}
+
+
+ }
+ 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;