mark all texts for translation #179

pull/205/head
Jon Eugster 2 years ago
parent 038dbe71b8
commit 7e9514fe96

@ -99,7 +99,7 @@ module.exports = {
}, },
lngs: ['en','de'], lngs: ['en','de'],
ns: [], ns: [],
defaultLng: 'en-GB', defaultLng: 'en',
defaultNs: 'translation', defaultNs: 'translation',
defaultValue: (lng, ns, key) => { defaultValue: (lng, ns, key) => {
if (lng === 'en') { if (lng === 'en') {
@ -139,15 +139,9 @@ module.exports = {
'use strict'; 'use strict';
const parser = this.parser; const parser = this.parser;
let count = 0;
parser.parseTransFromString(outputText); parser.parseTransFromString(outputText);
parser.parseFuncFromString(outputText); parser.parseFuncFromString(outputText);
if (count > 0) {
console.log(`[i18next-scanner] transform: count=${chalk.cyan(count)}, file=${chalk.yellow(JSON.stringify(file.relative))}`);
}
done(); done();
} }
), ),

@ -38,5 +38,54 @@
"back to games selection": "", "back to games selection": "",
"close inventory": "", "close inventory": "",
"show inventory": "", "show inventory": "",
"World": "" "World": "",
"Show more help!": "",
"Goal": "",
"Current Goal": "",
"Objects": "",
"Assumptions": "",
"Further Goals": "",
"No Goals": "",
"Loading goal…": "",
"Click somewhere in the Lean file to enable the infoview.": "",
"Waiting for Lean server to start…": "",
"Level completed! 🎉": "",
"Level completed with warnings 🎭": "",
"Retry proof from here": "",
"Active Goal": "",
"Crashed! Go to editor mode and fix your proof! Last server response:": "",
"Line": "",
"Character": "",
"Loading messages…": "",
"Execute": "",
"Definitions": "",
"Theorems": "",
"locked": "",
"disabled": "",
"new": "",
"Not unlocked yet": "",
"Not available in this level": "",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "",
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we-ll happily add yours.</p>": "",
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "",
"Prerequisites": "",
"Worlds": "",
"Levels": "",
"Language": "",
"Development notes": "",
"Adding new games": "",
"Funding": "",
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "",
"Delete Progress?": "",
"Delete": "",
"Download & Delete": "",
"Cancel": "",
"Layout": "",
"Always visible": "",
"Save my settings (in the browser store)": "",
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "",
"Upload Saved Progress": "",
"Load selected file": ""
} }

@ -38,5 +38,54 @@
"back to games selection": "back to games selection", "back to games selection": "back to games selection",
"close inventory": "close inventory", "close inventory": "close inventory",
"show inventory": "show inventory", "show inventory": "show inventory",
"World": "World" "World": "World",
"Show more help!": "Show more help!",
"Goal": "Goal",
"Current Goal": "Current Goal",
"Objects": "Objects",
"Assumptions": "Assumptions",
"Further Goals": "Further Goals",
"No Goals": "No Goals",
"Loading goal…": "Loading goal…",
"Click somewhere in the Lean file to enable the infoview.": "Click somewhere in the Lean file to enable the infoview.",
"Waiting for Lean server to start…": "Waiting for Lean server to start…",
"Level completed! 🎉": "Level completed! 🎉",
"Level completed with warnings 🎭": "Level completed with warnings 🎭",
"Retry proof from here": "Retry proof from here",
"Active Goal": "Active Goal",
"Crashed! Go to editor mode and fix your proof! Last server response:": "Crashed! Go to editor mode and fix your proof! Last server response:",
"Line": "Line",
"Character": "Character",
"Loading messages…": "Loading messages…",
"Execute": "Execute",
"Definitions": "Definitions",
"Theorems": "Theorems",
"locked": "locked",
"disabled": "disabled",
"new": "new",
"Not unlocked yet": "Not unlocked yet",
"Not available in this level": "Not available in this level",
"A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>": "A repository of learning games for the proof assistant <1>Lean</1> <i>(Lean 4)</i> and its mathematical library <5>mathlib</5>",
"No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.": "No Games loaded. Use <1>http://localhost:3000/#/g/local/FOLDER</1> to open a game directly from a local folder.",
"<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>": "<p>As this server runs lean on our university machines, it has a limited capacity. Our current estimate is about 70 simultaneous games. We hope to address and test this limitation better in the future.</p><1>Most aspects of the games and the infrastructure are still in development. Feel free to file a <1>GitHub Issue</1> about any problems you experience!</1>",
"<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we-ll happily add yours.</p>": "<0>If you are considering writing your own game, you should use the <1>GameSkeleton Github Repo</1> as a template and read <3>How to Create a Game</3>.</0><1>You can directly load your games into the server and play it using the correct URL. The <1>instructions above</1> also explain the details for how to load your game to the server. We'd like to encourage you to contact us if you have any questions.</1><p>Featured games on this page are added manually. Please get in contact and we-ll happily add yours.</p>",
"This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.": "This server has been developed as part of the project <1>ADAM : Anticipating the Digital Age of Mathematics</1> at Heinrich-Heine-Universität in Düsseldorf.",
"Prerequisites": "Prerequisites",
"Worlds": "Worlds",
"Levels": "Levels",
"Language": "Language",
"Development notes": "Development notes",
"Adding new games": "Adding new games",
"Funding": "Funding",
"<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>": "<p>Do you want to delete your saved progress irreversibly?</p><p>(This deletes your proofs and your collected inventory. Saves from other games are not deleted.)</p>",
"Delete Progress?": "Delete Progress?",
"Delete": "Delete",
"Download & Delete": "Download & Delete",
"Cancel": "Cancel",
"Layout": "Layout",
"Always visible": "Always visible",
"Save my settings (in the browser store)": "Save my settings (in the browser store)",
"<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>": "<p>Select a JSON file with the saved game progress to load your progress.</p><1><0>Warning:</0> This will delete your current game progress! Consider <2>downloading your current progress</2> first!</1>",
"Upload Saved Progress": "Upload Saved Progress",
"Load selected file": "Load selected file"
} }

@ -13,13 +13,14 @@ import { changedOpenedIntro, selectCompleted, selectDifficulty, selectProgress }
import { useAppDispatch, useAppSelector } from '../hooks' import { useAppDispatch, useAppSelector } from '../hooks'
import { Button } from './button' import { Button } from './button'
import { downloadProgress } from './popup/erase' import { downloadProgress } from './popup/erase'
import { t } from 'i18next' import { useTranslation } from 'react-i18next'
/** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */ /** navigation buttons for mobile welcome page to switch between intro/tree/inventory. */
function MobileNavButtons({pageNumber, setPageNumber}: function MobileNavButtons({pageNumber, setPageNumber}:
{ pageNumber: number, { pageNumber: number,
setPageNumber: any}) { setPageNumber: any}) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
// if `prevText` or `prevIcon` is set, show a button to go back // if `prevText` or `prevIcon` is set, show a button to go back
@ -64,6 +65,7 @@ function MenuButton({navOpen, setNavOpen}) {
* for the last level, this button turns into a button going back to the welcome page. * for the last level, this button turns into a button going back to the welcome page.
*/ */
function NextButton({worldSize, difficulty, completed, setNavOpen}) { function NextButton({worldSize, difficulty, completed, setNavOpen}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
return (levelId < worldSize ? return (levelId < worldSize ?
@ -84,6 +86,7 @@ function NextButton({worldSize, difficulty, completed, setNavOpen}) {
* only renders if the current level id is > 0. * only renders if the current level id is > 0.
*/ */
function PreviousButton({setNavOpen}) { function PreviousButton({setNavOpen}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
return (levelId > 0 && <> return (levelId > 0 && <>
@ -98,6 +101,7 @@ function PreviousButton({setNavOpen}) {
/** button to toggle between editor and typewriter */ /** button to toggle between editor and typewriter */
function InputModeButton({setNavOpen, isDropdown}) { function InputModeButton({setNavOpen, isDropdown}) {
const { t } = useTranslation()
const {levelId} = React.useContext(WorldLevelIdContext) const {levelId} = React.useContext(WorldLevelIdContext)
const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext) const {typewriterMode, setTypewriterMode, lockEditorMode} = React.useContext(InputModeContext)
@ -124,6 +128,7 @@ function InputModeButton({setNavOpen, isDropdown}) {
* Note: Do not translate the word "Impressum"! German GDPR needs this. * Note: Do not translate the word "Impressum"! German GDPR needs this.
*/ */
function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) { function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
const { t } = useTranslation()
return <Button className="btn btn-inverted" return <Button className="btn btn-inverted"
title={t("information, Impressum, privacy policy")} inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}> title={t("information, Impressum, privacy policy")} inverted="true" to="" onClick={(ev) => {toggleImpressum(ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} /> <FontAwesomeIcon icon={faCircleInfo} />
@ -132,12 +137,14 @@ function ImpressumButton({setNavOpen, toggleImpressum, isDropdown}) {
} }
function PreferencesButton({setNavOpen, togglePreferencesPopup}) { function PreferencesButton({setNavOpen, togglePreferencesPopup}) {
const { t } = useTranslation()
return <Button title={t("Preferences")} inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}> return <Button title={t("Preferences")} inverted="true" to="" onClick={() => {togglePreferencesPopup(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faGear} />&nbsp;{t("Preferences")} <FontAwesomeIcon icon={faGear} />&nbsp;{t("Preferences")}
</Button> </Button>
} }
function GameInfoButton({setNavOpen, toggleInfo}) { function GameInfoButton({setNavOpen, toggleInfo}) {
const { t } = useTranslation()
return <Button className="btn btn-inverted" return <Button className="btn btn-inverted"
title={t("Game Info & Credits")} inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}> title={t("Game Info & Credits")} inverted="true" to="" onClick={() => {toggleInfo(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faCircleInfo} />&nbsp;{t("Game Info")} <FontAwesomeIcon icon={faCircleInfo} />&nbsp;{t("Game Info")}
@ -145,18 +152,21 @@ function GameInfoButton({setNavOpen, toggleInfo}) {
} }
function EraseButton ({setNavOpen, toggleEraseMenu}) { function EraseButton ({setNavOpen, toggleEraseMenu}) {
const { t } = useTranslation()
return <Button title={t("Clear Progress")} inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}> return <Button title={t("Clear Progress")} inverted="true" to="" onClick={() => {toggleEraseMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faEraser} />&nbsp;{t("Erase")} <FontAwesomeIcon icon={faEraser} />&nbsp;{t("Erase")}
</Button> </Button>
} }
function DownloadButton ({setNavOpen, gameId, gameProgress}) { function DownloadButton ({setNavOpen, gameId, gameProgress}) {
const { t } = useTranslation()
return <Button title={t("Download Progress")} inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}> return <Button title={t("Download Progress")} inverted="true" to="" onClick={(ev) => {downloadProgress(gameId, gameProgress, ev); setNavOpen(false)}}>
<FontAwesomeIcon icon={faDownload} />&nbsp;{t("Download")} <FontAwesomeIcon icon={faDownload} />&nbsp;{t("Download")}
</Button> </Button>
} }
function UploadButton ({setNavOpen, toggleUploadMenu}) { function UploadButton ({setNavOpen, toggleUploadMenu}) {
const { t } = useTranslation()
return <Button title={t("Load Progress from JSON")} inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}> return <Button title={t("Load Progress from JSON")} inverted="true" to="" onClick={() => {toggleUploadMenu(); setNavOpen(false)}}>
<FontAwesomeIcon icon={faUpload} />&nbsp;{t("Upload")} <FontAwesomeIcon icon={faUpload} />&nbsp;{t("Upload")}
</Button> </Button>
@ -164,6 +174,7 @@ function UploadButton ({setNavOpen, toggleUploadMenu}) {
/** button to go back to welcome page */ /** button to go back to welcome page */
function HomeButton({isDropdown}) { function HomeButton({isDropdown}) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
return <Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn"> return <Button to={`/${gameId}`} inverted="true" title={t("back to world selection")} id="home-btn">
<FontAwesomeIcon icon={faHome} /> <FontAwesomeIcon icon={faHome} />
@ -172,6 +183,7 @@ function HomeButton({isDropdown}) {
} }
function LandingPageButton() { function LandingPageButton() {
const { t } = useTranslation()
return <Button inverted="false" title={t("back to games selection")} to="/"> return <Button inverted="false" title={t("back to games selection")} to="/">
<FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} /> <FontAwesomeIcon icon={faArrowLeft} />&nbsp;<FontAwesomeIcon icon={faGlobe} />
</Button> </Button>
@ -181,6 +193,7 @@ function LandingPageButton() {
* only displays a button if `setPageNumber` is set. * only displays a button if `setPageNumber` is set.
*/ */
function InventoryButton({pageNumber, setPageNumber}) { function InventoryButton({pageNumber, setPageNumber}) {
const { t } = useTranslation()
return (setPageNumber && return (setPageNumber &&
<Button to="" className="btn btn-inverted toggle-width" <Button to="" className="btn btn-inverted toggle-width"
title={pageNumber ? t("close inventory") : t("show inventory")} title={pageNumber ? t("close inventory") : t("show inventory")}
@ -239,6 +252,7 @@ export function LevelAppBar({isLoading, levelTitle, toggleImpressum, toggleInfo,
pageNumber?: number, pageNumber?: number,
setPageNumber?: any, setPageNumber?: any,
}) { }) {
const { t } = useTranslation()
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const {mobile} = React.useContext(PreferencesContext) const {mobile} = React.useContext(PreferencesContext)

@ -4,6 +4,7 @@ import Markdown from './markdown';
import { DeletedChatContext, ProofContext } from "./infoview/context"; import { DeletedChatContext, ProofContext } from "./infoview/context";
import { lastStepHasErrors } from "./infoview/goals"; import { lastStepHasErrors } from "./infoview/goals";
import { Button } from "./button"; import { Button } from "./button";
import { useTranslation } from "react-i18next";
/** Plug-in the variable names in a hint. We do this client-side to prepare /** Plug-in the variable names in a hint. We do this client-side to prepare
* for i18n in the future. i.e. one should be able translate the `rawText` * for i18n in the future. i.e. one should be able translate the `rawText`
@ -88,6 +89,8 @@ function hasHiddenHints(step: InteractiveGoalsWithHints): boolean {
export function MoreHelpButton({selected=null} : {selected?: number}) { export function MoreHelpButton({selected=null} : {selected?: number}) {
const { t } = useTranslation()
const {proof, setProof} = React.useContext(ProofContext) const {proof, setProof} = React.useContext(ProofContext)
const {deletedChat, setDeletedChat, showHelp, setShowHelp} = React.useContext(DeletedChatContext) const {deletedChat, setDeletedChat, showHelp, setShowHelp} = React.useContext(DeletedChatContext)
@ -113,7 +116,7 @@ export function MoreHelpButton({selected=null} : {selected?: number}) {
if (hasHiddenHints(proof?.steps[k]) && !showHelp.has(k)) { if (hasHiddenHints(proof?.steps[k]) && !showHelp.has(k)) {
return <Button to="" onClick={activateHiddenHints}> return <Button to="" onClick={activateHiddenHints}>
Show more help! {t("Show more help!")}
</Button> </Button>
} }
} }

@ -14,6 +14,7 @@ import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, Interacti
import { RpcSessionAtPos } from '@leanprover/infoview/*'; import { RpcSessionAtPos } from '@leanprover/infoview/*';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
import { useTranslation } from 'react-i18next';
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */ /** Returns true if `h` is inaccessible according to Lean's default name rendering. */
function isInaccessibleName(h: string): boolean { function isInaccessibleName(h: string): boolean {
@ -134,11 +135,6 @@ interface GoalProps {
typewriter: boolean typewriter: boolean
} }
interface ProofDisplayProps {
proof: string
}
/** /**
* Displays the hypotheses, target type and optional case label of a goal according to the * Displays the hypotheses, target type and optional case label of a goal according to the
* provided `filter`. */ * provided `filter`. */
@ -186,6 +182,7 @@ export const Goal = React.memo((props: GoalProps) => {
}) })
export const MainAssumptions = React.memo((props: GoalProps2) => { export const MainAssumptions = React.memo((props: GoalProps2) => {
let { t } = useTranslation()
const { goals, filter } = props const { goals, filter } = props
const goal = goals[0] const goal = goals[0]
@ -200,7 +197,7 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
[locs, goal.mvarId]) [locs, goal.mvarId])
const goalLi = <div key={'goal'}> const goalLi = <div key={'goal'}>
<div className="goal-title">Goal: </div> <div className="goal-title">{t("Goal") + ":"}</div>
<LocationsContext.Provider value={goalLocs}> <LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type} /> <InteractiveCode fmt={goal.type} />
</LocationsContext.Provider> </LocationsContext.Provider>
@ -210,25 +207,26 @@ export const MainAssumptions = React.memo((props: GoalProps2) => {
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption) const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
return <div id="main-assumptions"> return <div id="main-assumptions">
<div className="goals-section-title">Current Goal</div> <div className="goals-section-title">{t("Current Goal")}</div>
{filter.reverse && goalLi} {filter.reverse && goalLi}
{ objectHyps.length > 0 && { objectHyps.length > 0 &&
<div className="hyp-group"><div className="hyp-group-title">Objects:</div> <div className="hyp-group"><div className="hyp-group-title">{t("Objects") + ":"}</div>
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> } {objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
{ assumptionHyps.length > 0 && { assumptionHyps.length > 0 &&
<div className="hyp-group"> <div className="hyp-group">
<div className="hyp-group-title">Assumptions:</div> <div className="hyp-group-title">{t("Assumptions") + ":"}</div>
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)} {assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}
</div> } </div> }
</div> </div>
}) })
export const OtherGoals = React.memo((props: GoalProps2) => { export const OtherGoals = React.memo((props: GoalProps2) => {
let { t } = useTranslation()
const { goals, filter } = props const { goals, filter } = props
return <> return <>
{goals && goals.length > 1 && {goals && goals.length > 1 &&
<div id="other-goals" className="other-goals"> <div id="other-goals" className="other-goals">
<div className="goals-section-title">Further Goals</div> <div className="goals-section-title">{t("Further Goals")}</div>
{goals.slice(1).map((goal, i) => {goals.slice(1).map((goal, i) =>
<details key={i}> <details key={i}>
<summary> <summary>
@ -240,25 +238,6 @@ export const OtherGoals = React.memo((props: GoalProps2) => {
</> </>
}) })
// TODO: deprecated
export const ProofDisplay = React.memo((props : ProofDisplayProps) => {
const { proof } = props
const steps = proof.match(/.+/g)
return <>
{ steps &&
<div id="current-proof">
<div className="goals-section-title">Proof history</div>
<div className="proof-display-wrapper">
<div className="proof-display">
{steps.map((s) =>
<div>{s}</div>
)}
</div>
</div>
</div>}
</>
})
interface GoalsProps { interface GoalsProps {
goals: InteractiveGoalsWithHints goals: InteractiveGoalsWithHints
filter: GoalFilterState filter: GoalFilterState

@ -13,9 +13,10 @@ import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-i
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation' import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'
import { AllMessages, lspDiagToInteractive } from './messages' import { AllMessages, lspDiagToInteractive } from './messages'
import { goalsToString, Goal, MainAssumptions, OtherGoals, ProofDisplay } from './goals' import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api' import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context' import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
import { useTranslation } from 'react-i18next'
// TODO: All about pinning could probably be removed // TODO: All about pinning could probably be removed
type InfoKind = 'cursor' | 'pin' type InfoKind = 'cursor' | 'pin'
@ -87,6 +88,7 @@ interface InfoDisplayContentProps extends PausableProps {
} }
const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => { const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
let { t } = useTranslation()
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props
const hasWidget = userWidgets.length > 0 const hasWidget = userWidgets.length > 0
@ -131,7 +133,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
<div> <div>
{ goals && (goals.goals.length > 0 { goals && (goals.goals.length > 0
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} /> ? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
: <div className="goals-section-title">No Goals</div> : <div className="goals-section-title">{t("No Goals")}</div>
)} )}
</div> </div>
</LocationsContext.Provider> </LocationsContext.Provider>
@ -150,7 +152,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
{' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a> {' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a>
{' '}to see information. {' '}to see information.
</span> : </span> :
<><CircularProgress /><div>Loading goal...</div></>)} <><CircularProgress /><div>{t("Loading goal…")}</div></>)}
<AllMessages /> <AllMessages />
{/* <LocationsContext.Provider value={locs}> {/* <LocationsContext.Provider value={locs}>
{goals && goals.goals.length > 1 && <div className="goals-section other-goals"> {goals && goals.goals.length > 1 && <div className="goals-section other-goals">

@ -6,9 +6,11 @@ import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentCo
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { Info, InfoProps } from './info'; import { Info, InfoProps } from './info';
import { useTranslation } from 'react-i18next';
/** Manages and displays pinned infos, as well as info for the current location. */ /** Manages and displays pinned infos, as well as info for the current location. */
export function Infos() { export function Infos() {
let { t } = useTranslation()
const ec = React.useContext(EditorContext); const ec = React.useContext(EditorContext);
// Update pins when the document changes. In particular, when edits are made // Update pins when the document changes. In particular, when edits are made
@ -126,6 +128,6 @@ export function Infos() {
return <div> return <div>
{infoProps.map (ps => <Info {...ps} />)} {infoProps.map (ps => <Info {...ps} />)}
{!curPos && <p>Click somewhere in the Lean file to enable the infoview.</p> } {!curPos && <p>{t("Click somewhere in the Lean file to enable the infoview.")}</p> }
</div>; </div>;
} }

@ -37,6 +37,7 @@ import { store } from '../../state/store';
import { Hints, MoreHelpButton, filterHints } from '../hints'; import { Hints, MoreHelpButton, filterHints } from '../hints';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { DiagnosticSeverity } from 'vscode-languageclient'; import { DiagnosticSeverity } from 'vscode-languageclient';
import { useTranslation } from 'react-i18next';
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is /** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
* always present, or the monaco editor cannot start. * always present, or the monaco editor cannot start.
@ -151,6 +152,7 @@ function ExerciseStatement({ data, showLeanStatement = false }) {
// TODO: This is only used in `EditorInterface` // TODO: This is only used in `EditorInterface`
// while `TypewriterInterface` has this copy-pasted in. // while `TypewriterInterface` has this copy-pasted in.
export function Main(props: { world: string, level: number, data: LevelInfo}) { export function Main(props: { world: string, level: number, data: LevelInfo}) {
let { t } = useTranslation()
const ec = React.useContext(EditorContext); const ec = React.useContext(EditorContext);
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext) const {worldId, levelId} = React.useContext(WorldLevelIdContext)
@ -228,14 +230,14 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
// that we want to persist. // that we want to persist.
let ret let ret
if (!serverVersion) { if (!serverVersion) {
ret = <p>Waiting for Lean server to start...</p> ret = <p>{t("Waiting for Lean server to start…")}</p>
} else if (serverStoppedResult) { } else if (serverStoppedResult) {
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div> ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
} else { } else {
ret = <div className="infoview vscode-light"> ret = <div className="infoview vscode-light">
{proof?.completedWithWarnings && {proof?.completedWithWarnings &&
<div className="level-completed"> <div className="level-completed">
{proof?.completed ? "Level completed! 🎉" : "Level completed with warnings 🎭"} {proof?.completed ? t("Level completed! 🎉") : t("Level completed with warnings 🎭")}
</div> </div>
} }
<Infos /> <Infos />
@ -272,8 +274,8 @@ function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, dele
} else { } else {
return <div className="command"> return <div className="command">
<div className="command-text">{proof?.steps[i].command}</div> <div className="command-text">{proof?.steps[i].command}</div>
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}> <Button to="" className="undo-button btn btn-inverted" title={t("Retry proof from here")} onClick={deleteProof}>
<FontAwesomeIcon icon={faDeleteLeft} />&nbsp;Retry <FontAwesomeIcon icon={faDeleteLeft} />&nbsp;{this("Retry")}
</Button> </Button>
</div> </div>
} }
@ -332,7 +334,7 @@ function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, dele
/** The tabs of goals that lean ahs after the command of this step has been processed */ /** The tabs of goals that lean ahs after the command of this step has been processed */
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: InteractiveGoalsWithHints, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) { function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: InteractiveGoalsWithHints, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
let { t } = useTranslation()
const [selectedGoal, setSelectedGoal] = React.useState<number>(0) const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
if (proofStep.goals.length == 0) { if (proofStep.goals.length == 0) {
@ -344,7 +346,7 @@ function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofSte
{proofStep.goals.map((goal, i) => ( {proofStep.goals.map((goal, i) => (
// TODO: Should not use index as key. // TODO: Should not use index as key.
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}> <div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
{i ? `Goal ${i + 1}` : "Active Goal"} {i ? t("Goal") + ` ${i + 1}` : t("Active Goal")}
</div> </div>
))} ))}
</div> </div>
@ -389,6 +391,7 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
/** The interface in command line mode */ /** The interface in command line mode */
export function TypewriterInterface({props}) { export function TypewriterInterface({props}) {
let { t } = useTranslation()
const ec = React.useContext(EditorContext) const ec = React.useContext(EditorContext)
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const editor = React.useContext(MonacoEditorContext) const editor = React.useContext(MonacoEditorContext)
@ -500,8 +503,7 @@ export function TypewriterInterface({props}) {
<div className='proof' ref={proofPanelRef}> <div className='proof' ref={proofPanelRef}>
<ExerciseStatement data={props.data} /> <ExerciseStatement data={props.data} />
{crashed ? <div> {crashed ? <div>
<p className="crashed_message">Crashed! Go to editor mode and fix your proof! <p className="crashed_message">{t("Crashed! Go to editor mode and fix your proof! Last server response:")}</p>
Last server response:</p>
{interimDiags.map(diag => { {interimDiags.map(diag => {
const severityClass = diag.severity ? { const severityClass = diag.severity ? {
[DiagnosticSeverity.Error]: 'error', [DiagnosticSeverity.Error]: 'error',
@ -512,7 +514,7 @@ export function TypewriterInterface({props}) {
return <div> return <div>
<div className={`${severityClass} ml1 message`}> <div className={`${severityClass} ml1 message`}>
<p className="mv2">Line {diag.range.start.line}, Character {diag.range.start.character}</p> <p className="mv2">{t("Line")}&nbsp;{diag.range.start.line}, {t("Character")}&nbsp;{diag.range.start.character}</p>
<pre className="font-code pre-wrap"> <pre className="font-code pre-wrap">
{diag.message} {diag.message}
</pre> </pre>
@ -579,7 +581,7 @@ export function TypewriterInterface({props}) {
<div className="button-row mobile"> <div className="button-row mobile">
{props.level >= props.worldSize ? {props.level >= props.worldSize ?
<Button to={`/${gameId}`}> <Button to={`/${gameId}`}>
<FontAwesomeIcon icon={faHome} />&nbsp;Leave World <FontAwesomeIcon icon={faHome} />&nbsp;{t("Leave World")}
</Button> </Button>
: :
<Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}> <Button to={`/${gameId}/world/${props.world}/level/${props.level + 1}`}>

@ -11,6 +11,7 @@ import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions' import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'
import { InputModeContext } from './context' import { InputModeContext } from './context'
import { useTranslation } from 'react-i18next'
interface MessageViewProps { interface MessageViewProps {
uri: DocumentUri; uri: DocumentUri;
@ -202,6 +203,7 @@ export function AllMessages() {
/** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */ /** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */
function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: DocumentPosition | undefined , messages: () => Promise<InteractiveDiagnostic[]>}) { function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: DocumentPosition | undefined , messages: () => Promise<InteractiveDiagnostic[]>}) {
let { t } = useTranslation()
const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined) const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined)
React.useEffect(() => { void messages().then( React.useEffect(() => { void messages().then(
msgs => setMsgs(msgs.filter( msgs => setMsgs(msgs.filter(
@ -212,7 +214,7 @@ function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: Doc
return d.range.start.line == curPos.line return d.range.start.line == curPos.line
})) }))
) }, [messages, curPos]) ) }, [messages, curPos])
if (msgs === undefined) return <div>Loading messages...</div> if (msgs === undefined) return <div>{t("Loading messages…")}</div>
else return <MessagesList uri={uri} messages={msgs}/> else return <MessagesList uri={uri} messages={msgs}/>
} }

@ -20,15 +20,13 @@ import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context' import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context'
import { goalsToString, lastStepHasErrors, loadGoals } from './goals' import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
import { GameHint, ProofState } from './rpc_api' import { GameHint, ProofState } from './rpc_api'
import { useTranslation } from 'react-i18next'
export interface GameDiagnosticsParams { export interface GameDiagnosticsParams {
uri: DocumentUri; uri: DocumentUri;
diagnostics: Diagnostic[]; diagnostics: Diagnostic[];
} }
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */ /* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
// register Monaco languages // register Monaco languages
@ -73,6 +71,7 @@ monaco.languages.setLanguageConfiguration('lean4cmd', config);
/** The input field */ /** The input field */
export function Typewriter({disabled}: {disabled?: boolean}) { export function Typewriter({disabled}: {disabled?: boolean}) {
let { t } = useTranslation()
/** Reference to the hidden multi-line editor */ /** Reference to the hidden multi-line editor */
const editor = React.useContext(MonacoEditorContext) const editor = React.useContext(MonacoEditorContext)
@ -94,102 +93,6 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
const rpcSess = React.useContext(RpcContext) const rpcSess = React.useContext(RpcContext)
/** Load all goals an messages of the current proof (line-by-line) and save
* the retrieved information into context (`ProofContext`)
*/
// const loadAllGoals = React.useCallback(() => {
// let goalCalls = []
// let msgCalls = []
// // For each line of code ask the server for the goals and the messages on this line
// for (let i = 0; i < model.getLineCount(); i++) {
// goalCalls.push(
// rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
// )
// msgCalls.push(
// getInteractiveDiagnostics(rpcSess, {start: i, end: i+1}).catch((error) => {console.debug("promise broken")})
// )
// }
// // Wait for all these requests to be processed before saving the results
// Promise.all(goalCalls).then((steps : InteractiveGoalsWithHints[]) => {
// Promise.all(msgCalls).then((diagnostics : [InteractiveDiagnostic[]]) => {
// let tmpProof : ProofStep[] = []
// let goalCount = 0
// steps.map((goals, i) => {
// // The first step has an empty command and therefore also no error messages
// // Usually there is a newline at the end of the editors content, so we need to
// // display diagnostics from potentally two lines in the last step.
// let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : diagnostics[i-1]) : []
// // Filter out the 'unsolved goals' message
// messages = messages.filter((msg) => {
// return !("append" in msg.message &&
// "text" in msg.message.append[0] &&
// msg.message.append[0].text === "unsolved goals")
// })
// if (typeof goals == 'undefined') {
// tmpProof.push({
// command: i ? model.getLineContent(i) : '',
// goals: [],
// hints: [],
// errors: messages
// } as ProofStep)
// console.debug('goals is undefined')
// return
// }
// // If the number of goals reduce, show a message
// if (goals.length && goalCount > goals.length) {
// messages.unshift({
// range: {
// start: {
// line: i-1,
// character: 0,
// },
// end: {
// line: i-1,
// character: 0,
// }},
// severity: DiagnosticSeverity.Information,
// message: {
// text: 'intermediate goal solved 🎉'
// }
// })
// }
// goalCount = goals.length
// // with no goals there will be no hints.
// let hints : GameHint[] = goals.length ? goals[0].hints : []
// console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
// console.debug(`Goals: (${i}): `, goalsToString(goals)) //
// console.debug(`Hints: (${i}): `, hints)
// console.debug(`Errors: (${i}): `, messages)
// tmpProof.push({
// // the command of the line above. Note that `getLineContent` starts counting
// // at `1` instead of `zero`. The first ProofStep will have an empty command.
// command: i ? model.getLineContent(i) : '',
// // TODO: store correct data
// goals: goals.map(g => g.goal),
// // only need the hints of the active goals in chat
// hints: hints,
// // errors and messages from the server
// errors: messages
// } as ProofStep)
// })
// // Save the proof to the context
// setProof(tmpProof)
// }).catch((error) => {console.debug("promise broken")})
// }).catch((error) => {console.debug("promise broken")})
// }, [editor, rpcSess, uri, model])
// Run the command // Run the command
const runCommand = React.useCallback(() => { const runCommand = React.useCallback(() => {
if (processing) {return} if (processing) {return}
@ -355,7 +258,7 @@ export function Typewriter({disabled}: {disabled?: boolean}) {
<div ref={inputRef} className="typewriter-input" /> <div ref={inputRef} className="typewriter-input" />
</div> </div>
<button type="submit" disabled={processing} className="btn btn-inverted"> <button type="submit" disabled={processing} className="btn btn-inverted">
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute <FontAwesomeIcon icon={faWandMagicSparkles} />&nbsp;{t("Execute")}
</button> </button>
</form> </form>
</div> </div>

@ -10,6 +10,7 @@ import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadIn
import { selectDifficulty, selectInventory } from '../state/progress'; import { selectDifficulty, selectInventory } from '../state/progress';
import { store } from '../state/store'; import { store } from '../state/store';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { t } from 'i18next'; import { t } from 'i18next';
export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} : export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=false} :
@ -20,6 +21,7 @@ export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=
setLemmaTab: any, setLemmaTab: any,
enableAll?: boolean, enableAll?: boolean,
}) { }) {
const { t } = useTranslation()
return ( return (
<div className="inventory"> <div className="inventory">
@ -29,11 +31,11 @@ export function Inventory({levelInfo, openDoc, lemmaTab, setLemmaTab, enableAll=
{levelInfo?.tactics && {levelInfo?.tactics &&
<InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/> <InventoryList items={levelInfo?.tactics} docType="Tactic" openDoc={openDoc} enableAll={enableAll}/>
} }
<h2>Definitions</h2> <h2>{t("Definitions")}</h2>
{levelInfo?.definitions && {levelInfo?.definitions &&
<InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/> <InventoryList items={levelInfo?.definitions} docType="Definition" openDoc={openDoc} enableAll={enableAll}/>
} }
<h2>Theorems</h2> <h2>{t("Theorems")}</h2>
{levelInfo?.lemmas && {levelInfo?.lemmas &&
<InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/> <InventoryList items={levelInfo?.lemmas} docType="Lemma" openDoc={openDoc} level={levelInfo} enableAll={enableAll} tab={lemmaTab} setTab={setLemmaTab}/>
} }
@ -98,11 +100,11 @@ function InventoryList({items, docType, openDoc, tab=null, setTab=undefined, lev
function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, enableAll=false}) { function InventoryItem({item, name, displayName, locked, disabled, newly, showDoc, enableAll=false}) {
const icon = locked ? <FontAwesomeIcon icon={faLock} /> : const icon = locked ? <FontAwesomeIcon icon={faLock} /> :
disabled ? <FontAwesomeIcon icon={faBan} /> : item.st disabled ? <FontAwesomeIcon icon={faBan} /> : item.st
const className = locked ? "locked" : disabled ? "disabled" : newly ? "new" : "" const className = locked ? t("locked") : disabled ? t("disabled") : newly ? t("new") : ""
// Note: This is somewhat a hack as the statement of lemmas comes currently in the form // Note: This is somewhat a hack as the statement of lemmas comes currently in the form
// `Namespace.statement_name (x y : Nat) : some type` // `Namespace.statement_name (x y : Nat) : some type`
const title = locked ? "Not unlocked yet" : const title = locked ? t("Not unlocked yet") :
disabled ? "Not available in this level" : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '') disabled ? t("Not available in this level") : (item.altTitle ? item.altTitle.substring(item.altTitle.indexOf(' ') + 1) : '')
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import '@fontsource/roboto/300.css'; import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css';
@ -15,6 +15,8 @@ import {PrivacyPolicyPopup} from './popup/privacy_policy'
import { GameTile, useGetGameInfoQuery } from '../state/api' import { GameTile, useGetGameInfoQuery } from '../state/api'
import path from 'path'; import path from 'path';
import lean4gameConfig from '../config.json'
const flag = { const flag = {
'Dutch': '🇳🇱', 'Dutch': '🇳🇱',
'English': '🇬🇧', 'English': '🇬🇧',
@ -36,7 +38,7 @@ function GithubIcon({url='https://github.com'}) {
} }
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) { function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
let { t } = useTranslation()
let navigate = useNavigate(); let navigate = useNavigate();
const routeChange = () =>{ const routeChange = () =>{
navigate(gameId); navigate(gameId);
@ -57,19 +59,19 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
<table className="info"> <table className="info">
<tbody> <tbody>
<tr> <tr>
<td title="consider playing these games first.">Prerequisites</td> <td title="consider playing these games first.">{t("Prerequisites")}</td>
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td> <td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
</tr> </tr>
<tr> <tr>
<td>Worlds</td> <td>{t("Worlds")}</td>
<td>{data.worlds}</td> <td>{data.worlds}</td>
</tr> </tr>
<tr> <tr>
<td>Levels</td> <td>{t("Levels")}</td>
<td>{data.levels}</td> <td>{data.levels}</td>
</tr> </tr>
<tr> <tr>
<td>Language</td> <td>{t("Language")}</td>
<td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td> <td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
</tr> </tr>
</tbody> </tbody>
@ -86,59 +88,7 @@ function LandingPage() {
const openImpressum = () => setImpressum(true); const openImpressum = () => setImpressum(true);
const closeImpressum = () => setImpressum(false); const closeImpressum = () => setImpressum(false);
// const [allGames, setAllGames] = React.useState([]) let allTiles = lean4gameConfig.allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
// const [allTiles, setAllTiles] = React.useState([])
// const getTiles=()=>{
// fetch('featured_games.json', {
// headers : {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json'
// }
// }
// ).then(function(response){
// return response.json()
// }).then(function(data) {
// setAllGames(data.featured_games)
// })
// }
// React.useEffect(()=>{
// getTiles()
// },[])
// React.useEffect(()=>{
// Promise.allSettled(
// allGames.map((gameId) => (
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
// ).then(responses =>
// responses.forEach((result) => console.log(result)))
// // Promise.all(responses.map(res => {
// // if (res.status == "fulfilled") {
// // console.log(res.value.json())
// // return res.value.json()
// // } else {
// // return undefined
// // }
// // }))
// // ).then(allData => {
// // setAllTiles(allData.map(data => data?.tile))
// // })
// },[allGames])
// TODO: I would like to read the supported games list form a JSON,
// Then load all these games in
//
let allGames = [
"leanprover-community/nng4",
"hhu-adam/robo",
"djvelleman/stg4",
"miguelmarco/stg4",
"trequetrum/lean4game-logic",
]
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
return <div className="landing-page"> return <div className="landing-page">
@ -155,18 +105,23 @@ function LandingPage() {
<div id="main-title"> <div id="main-title">
<h1>{t("Lean Game Server")}</h1> <h1>{t("Lean Game Server")}</h1>
<p> <p>
<Trans>
A repository of learning games for the A repository of learning games for the
proof assistant <a target="_blank" href="https://leanprover-community.github.io/">Lean</a> <i>(Lean 4)</i> and proof assistant <a target="_blank" href="https://leanprover-community.github.io/">Lean</a> <i>(Lean 4)</i> and
its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a> its mathematical library <a target="_blank" href="https://github.com/leanprover-community/mathlib4">mathlib</a>
</Trans>
</p> </p>
</div> </div>
</header> </header>
<div className="game-list"> <div className="game-list">
{allTiles.length == 0 ? {allTiles.length == 0 ?
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a <p>
<Trans>
No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
game directly from a local folder. game directly from a local folder.
</Trans>
</p> </p>
: allGames.map((id, i) => ( : lean4gameConfig.allGames.map((id, i) => (
<Tile <Tile
key={id} key={id}
gameId={`g/${id}`} gameId={`g/${id}`}
@ -177,7 +132,8 @@ function LandingPage() {
</div> </div>
<section> <section>
<div className="wrapper"> <div className="wrapper">
<h2>Development notes</h2> <h2>{t("Development notes")}</h2>
<Trans>
<p> <p>
As this server runs lean on our university machines, it has a limited capacity. As this server runs lean on our university machines, it has a limited capacity.
Our current estimate is about 70 simultaneous games. Our current estimate is about 70 simultaneous games.
@ -188,11 +144,13 @@ function LandingPage() {
file a <a target="_blank" href="https://github.com/leanprover-community/lean4game/issues">GitHub Issue</a> about file a <a target="_blank" href="https://github.com/leanprover-community/lean4game/issues">GitHub Issue</a> about
any problems you experience! any problems you experience!
</p> </p>
</Trans>
</div> </div>
</section> </section>
<section> <section>
<div className="wrapper"> <div className="wrapper">
<h2>Adding new games</h2> <h2>{t("Adding new games")}</h2>
<Trans>
<p> <p>
If you are considering writing your own game, you should use If you are considering writing your own game, you should use
the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as the <a target="_blank" href="https://github.com/hhu-adam/GameSkeleton">GameSkeleton Github Repo</a> as
@ -209,19 +167,23 @@ function LandingPage() {
Featured games on this page are added manually. Featured games on this page are added manually.
Please get in contact and we-ll happily add yours. Please get in contact and we-ll happily add yours.
</p> </p>
</Trans>
</div> </div>
</section> </section>
<section> <section>
<div className="wrapper"> <div className="wrapper">
<h2>Funding</h2> <h2>{t("Funding")}</h2>
<p> <p>
<Trans>
This server has been developed as part of the This server has been developed as part of the
project <a target="_blank" href="https://hhu-adam.github.io">ADAM : Anticipating the Digital Age of Mathematics</a> at project <a target="_blank" href="https://hhu-adam.github.io">ADAM : Anticipating the Digital Age of Mathematics</a> at
Heinrich-Heine-Universität in Düsseldorf. Heinrich-Heine-Universität in Düsseldorf.
</Trans>
</p> </p>
</div> </div>
</section> </section>
<footer> <footer>
{/* Do not translate "Impressum", it's needed for German GDPR */}
<a className="link" onClick={openImpressum}>Impressum</a> <a className="link" onClick={openImpressum}>Impressum</a>
{impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null} {impressum? <PrivacyPolicyPopup handleClose={closeImpressum} />: null}
</footer> </footer>

@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
import { deleteProgress, selectProgress } from '../../state/progress' import { deleteProgress, selectProgress } from '../../state/progress'
import { downloadFile } from '../world_tree' import { downloadFile } from '../world_tree'
import { Button } from '../button' import { Button } from '../button'
import { Trans, useTranslation } from 'react-i18next'
/** download the current progress (i.e. what's saved in the browser store) */ /** download the current progress (i.e. what's saved in the browser store) */
export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) { export function downloadProgress(gameId: string, gameProgress: any, ev: React.MouseEvent) {
@ -25,6 +26,7 @@ export function downloadProgress(gameId: string, gameProgress: any, ev: React.Mo
* controlled by the containing element. * controlled by the containing element.
*/ */
export function ErasePopup ({handleClose}) { export function ErasePopup ({handleClose}) {
let { t } = useTranslation()
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const gameProgress = useSelector(selectProgress(gameId)) const gameProgress = useSelector(selectProgress(gameId))
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -43,17 +45,17 @@ export function ErasePopup ({handleClose}) {
<div className="modal-backdrop" onClick={handleClose} /> <div className="modal-backdrop" onClick={handleClose} />
<div className="modal"> <div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div> <div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h2>Delete Progress?</h2> <h2>{t("Delete Progress?")}</h2>
<Trans>
<p>Do you want to delete your saved progress irreversibly?</p> <p>Do you want to delete your saved progress irreversibly?</p>
<p> <p>
(This deletes your proofs and your collected inventory. (This deletes your proofs and your collected inventory.
Saves from other games are not deleted.) Saves from other games are not deleted.)
</p> </p>
</Trans>
<Button onClick={eraseProgress} to="">Delete</Button> <Button onClick={eraseProgress} to="">{t("Delete")}</Button>
<Button onClick={downloadAndErase} to="">Download & Delete</Button> <Button onClick={downloadAndErase} to="">{t("Download & Delete")}</Button>
<Button onClick={handleClose} to="">Cancel</Button> <Button onClick={handleClose} to="">{t("Cancel")}</Button>
</div> </div>
</div> </div>
} }

@ -4,18 +4,20 @@ import Markdown from '../markdown'
import { Switch, Button, ButtonGroup } from '@mui/material'; import { Switch, Button, ButtonGroup } from '@mui/material';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider'; import Slider from '@mui/material/Slider';
import supportedLanguages from './language_config.json' import lean4gameConfig from '../../config.json'
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import { IPreferencesContext } from "../infoview/context" import { IPreferencesContext } from "../infoview/context"
import ReactCountryFlag from 'react-country-flag'; import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
interface PreferencesPopupProps extends Omit<IPreferencesContext, 'mobile'> { interface PreferencesPopupProps extends Omit<IPreferencesContext, 'mobile'> {
handleClose: () => void handleClose: () => void
} }
export function PreferencesPopup({ layout, setLayout, isSavePreferences, language, setIsSavePreferences, handleClose, setLanguage }: PreferencesPopupProps) { export function PreferencesPopup({ layout, setLayout, isSavePreferences, language, setIsSavePreferences, handleClose, setLanguage }: PreferencesPopupProps) {
let { t } = useTranslation()
const marks = [ const marks = [
{ {
@ -50,7 +52,7 @@ export function PreferencesPopup({ layout, setLayout, isSavePreferences, languag
<Typography variant="body1" component="div" className="settings"> <Typography variant="body1" component="div" className="settings">
<div className='preferences-category'> <div className='preferences-category'>
<div className='category-title'> <div className='category-title'>
<h3>Language</h3> <h3>{t("Language")}</h3>
</div> </div>
<div className='preferences-item first leave-left-gap'> <div className='preferences-item first leave-left-gap'>
<FormControlLabel <FormControlLabel
@ -58,9 +60,9 @@ export function PreferencesPopup({ layout, setLayout, isSavePreferences, languag
<Box sx={{ width: 300 }}> <Box sx={{ width: 300 }}>
<Select <Select
value={language} value={language}
label={"Language"} label={t("Language")}
onChange={handlerChangeLanguage}> onChange={handlerChangeLanguage}>
{supportedLanguages.languages.map(lang => {return <MenuItem key={`menu-item-lang-${lang.iso}`} value={lang.iso}><ReactCountryFlag countryCode={lang.flag}/>&nbsp;{lang.name}</MenuItem>})} {lean4gameConfig.languages.map(lang => {return <MenuItem key={`menu-item-lang-${lang.iso}`} value={lang.iso}><ReactCountryFlag countryCode={lang.flag}/>&nbsp;{lang.name}</MenuItem>})}
</Select> </Select>
</Box> </Box>
} }
@ -70,14 +72,14 @@ export function PreferencesPopup({ layout, setLayout, isSavePreferences, languag
</div> </div>
<div className='preferences-category'> <div className='preferences-category'>
<div className='category-title'> <div className='category-title'>
<h3>Layout</h3> <h3>{t("Layout")}</h3>
</div> </div>
<div className='preferences-item first leave-left-gap'> <div className='preferences-item first leave-left-gap'>
<FormControlLabel <FormControlLabel
control={ control={
<Box sx={{ width: 300 }}> <Box sx={{ width: 300 }}>
<Slider <Slider
aria-label="Always visible" aria-label={t("Always visible")}
value={marks.find(item => item.key === layout).value} value={marks.find(item => item.key === layout).value}
step={1} step={1}
marks={marks} marks={marks}
@ -105,7 +107,7 @@ export function PreferencesPopup({ layout, setLayout, isSavePreferences, languag
color="primary" color="primary"
/> />
} }
label="Save my settings (in the browser store)" label={t("Save my settings (in the browser store)")}
labelPlacement="end" labelPlacement="end"
/> />
</div> </div>

@ -9,6 +9,8 @@ import * as React from 'react'
* *
* `handleClose` is the function to close it again because it's open/closed state is * `handleClose` is the function to close it again because it's open/closed state is
* controlled by the containing element. * controlled by the containing element.
*
* Note: Do not translate the Impressum!
*/ */
export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) { export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
return <div className="privacy-policy modal-wrapper"> return <div className="privacy-policy modal-wrapper">
@ -57,19 +59,3 @@ export function PrivacyPolicyPopup ({handleClose}: {handleClose: () => void}) {
</div> </div>
</div> </div>
} }
export const PrivacyPolicy: React.FC = () => {
const [open, setOpen] = React.useState(false)
const handleOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
return (
<>
<div className="privacy" onClick={handleOpen} title="Privacy Policy &amp; Impressum">
<FontAwesomeIcon icon={faShield} />
<p className="p1">legal</p>
<p className="p2">notes</p>
</div>
{open ? <PrivacyPolicyPopup handleClose={handleClose} /> : null}
</>
)
}

@ -10,7 +10,7 @@ import { Trans, useTranslation } from 'react-i18next'
* controlled by the containing element. * controlled by the containing element.
*/ */
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) { export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
const { t, i18n } = useTranslation() const { t } = useTranslation()
return <div className="privacy-policy modal-wrapper"> return <div className="privacy-policy modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} /> <div className="modal-backdrop" onClick={handleClose} />

@ -8,6 +8,7 @@ import { useAppDispatch } from '../../hooks'
import { GameProgressState, loadProgress, selectProgress } from '../../state/progress' import { GameProgressState, loadProgress, selectProgress } from '../../state/progress'
import { downloadFile } from '../world_tree' import { downloadFile } from '../world_tree'
import { Button } from '../button' import { Button } from '../button'
import { Trans, useTranslation } from 'react-i18next'
/** Pop-up that is displaying the Game Info. /** Pop-up that is displaying the Game Info.
* *
@ -15,6 +16,8 @@ import { Button } from '../button'
* controlled by the containing element. * controlled by the containing element.
*/ */
export function UploadPopup ({handleClose}) { export function UploadPopup ({handleClose}) {
let { t } = useTranslation()
const [file, setFile] = React.useState<File>(); const [file, setFile] = React.useState<File>();
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const gameProgress = useSelector(selectProgress(gameId)) const gameProgress = useSelector(selectProgress(gameId))
@ -54,17 +57,19 @@ export function UploadPopup ({handleClose}) {
<div className="modal-backdrop" onClick={handleClose} /> <div className="modal-backdrop" onClick={handleClose} />
<div className="modal"> <div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div> <div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h2>Upload Saved Progress</h2> <h2>{t("Upload Saved Progress")}</h2>
<Trans>
<p>Select a JSON file with the saved game progress to load your progress.</p> <p>Select a JSON file with the saved game progress to load your progress.</p>
<p><b>Warning:</b> This will delete your current game progress! <p><b>Warning:</b> This will delete your current game progress!
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p> Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!
</p>
</Trans>
<p> <p>
<input type="file" onChange={handleFileChange}/> <input type="file" onChange={handleFileChange}/>
</p> </p>
<Button to="" onClick={uploadProgress}>Load selected file</Button> <Button to="" onClick={uploadProgress}>{t("Load selected file")}</Button>
</div> </div>
</div> </div>
} }

@ -1,4 +1,12 @@
{ {
"allGames": [
"leanprover-community/nng4",
"hhu-adam/robo",
"djvelleman/stg4",
"miguelmarco/stg4",
"trequetrum/lean4game-logic"
],
"languages": [ "languages": [
{ {
"iso": "en", "iso": "en",
@ -11,4 +19,5 @@
"name": "Deutsch" "name": "Deutsch"
} }
] ]
} }
Loading…
Cancel
Save