support translations of the games
This commit is contained in:
@@ -19,6 +19,7 @@ import { PreferencesPopup } from './popup/preferences';
|
|||||||
import { ImpressumButton, MenuButton, PreferencesButton } from './app_bar';
|
import { ImpressumButton, MenuButton, PreferencesButton } from './app_bar';
|
||||||
import ReactCountryFlag from 'react-country-flag';
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
import lean4gameConfig from '../config.json'
|
import lean4gameConfig from '../config.json'
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
function GithubIcon({url='https://github.com'}) {
|
function GithubIcon({url='https://github.com'}) {
|
||||||
|
|
||||||
@@ -32,7 +33,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 { t, i18n } = useTranslation()
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const routeChange = () =>{
|
const routeChange = () =>{
|
||||||
navigate(gameId);
|
navigate(gameId);
|
||||||
@@ -42,19 +43,22 @@ function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
|
|||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the namespace of the game
|
||||||
|
i18next.loadNamespaces(gameId)
|
||||||
|
|
||||||
return <div className="game" onClick={routeChange}>
|
return <div className="game" onClick={routeChange}>
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<div className="title">{data.title}</div>
|
<div className="title">{t(data.title, { ns: gameId })}</div>
|
||||||
<div className="short-description">{data.short}
|
<div className="short-description">{t(data.short, { ns: gameId })}
|
||||||
</div>
|
</div>
|
||||||
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
|
||||||
<div className="long description"><Markdown>{data.long}</Markdown></div>
|
<div className="long description"><Markdown>{t(data.long, { ns: gameId })}</Markdown></div>
|
||||||
</div>
|
</div>
|
||||||
<table className="info">
|
<table className="info">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td title="consider playing these games first.">{t("Prerequisites")}</td>
|
<td title="consider playing these games first.">{t("Prerequisites")}</td>
|
||||||
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
|
<td><Markdown>{t(data.prerequisites.join(', '), { ns: gameId })}</Markdown></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{t("Worlds")}</td>
|
<td>{t("Worlds")}</td>
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ import { onigasmH } from 'onigasm/lib/onigasmH'
|
|||||||
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
|
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
|
||||||
import { InfoPopup } from './popup/game_info'
|
import { InfoPopup } from './popup/game_info'
|
||||||
import { PreferencesPopup } from './popup/preferences'
|
import { PreferencesPopup } from './popup/preferences'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
|
||||||
monacoSetup()
|
monacoSetup()
|
||||||
@@ -63,6 +65,10 @@ function Level() {
|
|||||||
const worldId = params.worldId
|
const worldId = params.worldId
|
||||||
|
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
// Load the namespace of the game
|
||||||
|
i18next.loadNamespaces(gameId)
|
||||||
|
|
||||||
const gameInfo = useGetGameInfoQuery({game: gameId})
|
const gameInfo = useGetGameInfoQuery({game: gameId})
|
||||||
|
|
||||||
// pop-ups
|
// pop-ups
|
||||||
@@ -88,6 +94,7 @@ function Level() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChatPanel({lastLevel, visible = true}) {
|
function ChatPanel({lastLevel, visible = true}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const chatRef = useRef<HTMLDivElement>(null)
|
const chatRef = useRef<HTMLDivElement>(null)
|
||||||
const {mobile} = useContext(PreferencesContext)
|
const {mobile} = useContext(PreferencesContext)
|
||||||
const gameId = useContext(GameIdContext)
|
const gameId = useContext(GameIdContext)
|
||||||
@@ -134,7 +141,7 @@ function ChatPanel({lastLevel, visible = true}) {
|
|||||||
// // chatRef.current!.scrollTo(0,0)
|
// // chatRef.current!.scrollTo(0,0)
|
||||||
// }, [gameId, worldId, levelId])
|
// }, [gameId, worldId, levelId])
|
||||||
|
|
||||||
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
|
let introText: Array<string> = t(level?.data?.introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
return <div className={`chat-panel ${visible ? '' : 'hidden'}`}>
|
||||||
<div ref={chatRef} className="chat">
|
<div ref={chatRef} className="chat">
|
||||||
@@ -441,11 +448,12 @@ function PlayableLevel({impressum, setImpressum, toggleInfo, togglePreferencesPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function IntroductionPanel({gameInfo}) {
|
function IntroductionPanel({gameInfo}) {
|
||||||
|
let { t } = useTranslation()
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
const {worldId} = useContext(WorldLevelIdContext)
|
const {worldId} = useContext(WorldLevelIdContext)
|
||||||
const {mobile} = React.useContext(PreferencesContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
let text: Array<string> = gameInfo.data?.worlds.nodes[worldId].introduction.split(/\n(\s*\n)+/)
|
let text: Array<string> = t(gameInfo.data?.worlds.nodes[worldId].introduction, {ns: gameId}).split(/\n(\s*\n)+/)
|
||||||
|
|
||||||
return <div className="chat-panel">
|
return <div className="chat-panel">
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
|
|||||||
@@ -23,19 +23,24 @@ import { WorldTreePanel } from './world_tree'
|
|||||||
import '../css/welcome.css'
|
import '../css/welcome.css'
|
||||||
import { WelcomeAppBar } from './app_bar'
|
import { WelcomeAppBar } from './app_bar'
|
||||||
import { Hint } from './hints'
|
import { Hint } from './hints'
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
|
||||||
/** the panel showing the game's introduction text */
|
/** the panel showing the game's introduction text */
|
||||||
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
function IntroductionPanel({introduction, setPageNumber}: {introduction: string, setPageNumber}) {
|
||||||
const {mobile} = React.useContext(PreferencesContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
let { t } = useTranslation()
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
|
// TODO: I left the setup for splitting up the introduction in place, but if it's not needed
|
||||||
// then this can be simplified.
|
// then this can be simplified.
|
||||||
|
|
||||||
// let text: Array<string> = introduction.split(/\n(\s*\n)+/)
|
// let text: Array<string> = introduction.split(/\n(\s*\n)+/)
|
||||||
let text: Array<string> = introduction ? [introduction] : []
|
let text: Array<string> = introduction ? [t(introduction, {ns : gameId})] : []
|
||||||
|
|
||||||
return <div className="column chat-panel">
|
return <div className="column chat-panel">
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
@@ -64,6 +69,10 @@ function IntroductionPanel({introduction, setPageNumber}: {introduction: string,
|
|||||||
/** main page of the game showing among others the tree of worlds/levels */
|
/** main page of the game showing among others the tree of worlds/levels */
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
const gameId = React.useContext(GameIdContext)
|
const gameId = React.useContext(GameIdContext)
|
||||||
|
|
||||||
|
// Load the namespace of the game
|
||||||
|
i18next.loadNamespaces(gameId)
|
||||||
|
|
||||||
const {mobile} = React.useContext(PreferencesContext)
|
const {mobile} = React.useContext(PreferencesContext)
|
||||||
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
const {layout, isSavePreferences, language, setLayout, setIsSavePreferences, setLanguage} = React.useContext(PreferencesContext)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ i18n
|
|||||||
.use(initReactI18next) // passes i18n down to react-i18next
|
.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
.init({
|
.init({
|
||||||
|
ns: ['translation'],
|
||||||
|
backend: {
|
||||||
|
// see https://github.com/i18next/i18next-http-backend
|
||||||
|
|
||||||
|
loadPath: function(lngs, namespaces: Array<string>) {
|
||||||
|
console.warn(namespaces)
|
||||||
|
if (namespaces[0].startsWith("g/")) {
|
||||||
|
return '/i18n/{{ns}}/{{lng}}/Game.json';
|
||||||
|
} else {
|
||||||
|
return '/locales/{{lng}}/{{ns}}.json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
||||||
// if you're using a language detector, do not define the lng option
|
// if you're using a language detector, do not define the lng option
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ router.get('/import/trigger/:owner/:repo', importTrigger)
|
|||||||
|
|
||||||
const server = app
|
const server = app
|
||||||
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) // TODO: add a dist folder from inside the game
|
||||||
|
.use('/i18n/g/:owner/:repo/:lang/*', (req, res, next) => {
|
||||||
|
const owner = req.params.owner;
|
||||||
|
const repo = req.params.repo
|
||||||
|
const lang = req.params.lang
|
||||||
|
const filename = req.params[0];
|
||||||
|
req.url = filename;
|
||||||
|
express.static(path.join(getGameDir(owner,repo),".i18n",lang))(req, res, next);
|
||||||
|
})
|
||||||
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
.use('/data/g/:owner/:repo/*', (req, res, next) => {
|
||||||
const owner = req.params.owner;
|
const owner = req.params.owner;
|
||||||
const repo = req.params.repo
|
const repo = req.params.repo
|
||||||
|
|||||||
@@ -331,6 +331,9 @@ structure GameParams where
|
|||||||
diagnostics : GameDiagnostics
|
diagnostics : GameDiagnostics
|
||||||
deriving ToJson, FromJson
|
deriving ToJson, FromJson
|
||||||
|
|
||||||
|
-- `snap` and `initParams` are unused
|
||||||
|
set_option linter.unusedVariables false in
|
||||||
|
|
||||||
/-- WIP: publish diagnostics, all intermediate goals and if the game is completed. -/
|
/-- WIP: publish diagnostics, all intermediate goals and if the game is completed. -/
|
||||||
def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.InitializeParams) (hOut : FS.Stream) :
|
def publishProofState (m : DocumentMeta) (snap : Snapshot) (initParams : Lsp.InitializeParams) (hOut : FS.Stream) :
|
||||||
IO Unit := do
|
IO Unit := do
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ open Lean.Parser Term
|
|||||||
open PrettyPrinter Delaborator SubExpr
|
open PrettyPrinter Delaborator SubExpr
|
||||||
open TSyntax.Compat
|
open TSyntax.Compat
|
||||||
|
|
||||||
|
|
||||||
#check Command.declSig
|
|
||||||
|
|
||||||
open private shouldGroupWithNext evalSyntaxConstant from Lean.PrettyPrinter.Delaborator.Builtins
|
open private shouldGroupWithNext evalSyntaxConstant from Lean.PrettyPrinter.Delaborator.Builtins
|
||||||
|
|
||||||
-- def typeSpec := leading_parser " :\\n: " >> termParser
|
-- def typeSpec := leading_parser " :\\n: " >> termParser
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name))
|
|||||||
|
|
||||||
IO.FS.writeFile (path / inventoryFileName) (toString (toJson inventory))
|
IO.FS.writeFile (path / inventoryFileName) (toString (toJson inventory))
|
||||||
|
|
||||||
-- write PO file for translation
|
-- write file for translation
|
||||||
I18n.createPOTemplate
|
I18n.createTemplate
|
||||||
|
|
||||||
open GameData
|
open GameData
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ If names are provided, it will introduce as many `let` statements as there are n
|
|||||||
syntax (name := letIntros) "let_intros" : tactic
|
syntax (name := letIntros) "let_intros" : tactic
|
||||||
-- (ppSpace colGt (ident <|> hole))*
|
-- (ppSpace colGt (ident <|> hole))*
|
||||||
|
|
||||||
#check letIntros
|
|
||||||
|
|
||||||
@[tactic letIntros] def evalLetIntros : Tactic := fun stx => do
|
@[tactic letIntros] def evalLetIntros : Tactic := fun stx => do
|
||||||
match stx with
|
match stx with
|
||||||
| `(tactic| let_intros) => liftMetaTactic fun mvarId => do
|
| `(tactic| let_intros) => liftMetaTactic fun mvarId => do
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
{"url": "https://github.com/hhu-adam/lean-i18n.git",
|
{"url": "https://github.com/hhu-adam/lean-i18n.git",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"subDir": null,
|
"subDir": null,
|
||||||
"rev": "c5b84feffb28dbd5b1ac74b3bf63271296fabfa5",
|
"rev": "59fea5bbc2ea22c2ee77ea6a65b0212ac241a56f",
|
||||||
"name": "i18n",
|
"name": "i18n",
|
||||||
"manifestFile": "lake-manifest.json",
|
"manifestFile": "lake-manifest.json",
|
||||||
"inputRev": "v4.6.0",
|
"inputRev": "main",
|
||||||
"inherited": false,
|
"inherited": false,
|
||||||
"configFile": "lakefile.lean"}],
|
"configFile": "lakefile.lean"}],
|
||||||
"name": "GameServer",
|
"name": "GameServer",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ package GameServer
|
|||||||
def leanVersion := "v4.6.0" -- TODO
|
def leanVersion := "v4.6.0" -- TODO
|
||||||
|
|
||||||
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
require std from git "https://github.com/leanprover/std4.git" @ leanVersion
|
||||||
require i18n from git "https://github.com/hhu-adam/lean-i18n.git" @ leanVersion
|
require i18n from git "https://github.com/hhu-adam/lean-i18n.git" @ "main" -- leanVersion
|
||||||
|
|
||||||
lean_lib GameServer
|
lean_lib GameServer
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ export default defineConfig({
|
|||||||
'/data': {
|
'/data': {
|
||||||
target: 'http://localhost:8080',
|
target: 'http://localhost:8080',
|
||||||
},
|
},
|
||||||
|
'/i18n': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user