diff --git a/client/i18next-scanner.config.cjs b/client/i18next-scanner.config.cjs
new file mode 100644
index 00000000..aa0c6ad3
--- /dev/null
+++ b/client/i18next-scanner.config.cjs
@@ -0,0 +1,101 @@
+const typescriptTransform = require('i18next-scanner-typescript');
+
+const fs = require('fs');
+const chalk = require('chalk');
+
+module.exports = {
+ input: [
+ 'client/src/**/*.{tsx,ts}',
+ // Use ! to filter out files or directories
+ '!client/i18n/**',
+ '!**/node_modules/**',
+ ],
+ output: './client/public/locales',
+ options: {
+ debug: true,
+ func: {
+ list: ['i18next.t', 'i18n.t', 't'],
+ extensions: ['.js', '.jsx'] // not .ts or .tsx since we use i18next-scanner-typescript!
+ },
+ trans: {
+ component: 'Trans',
+ i18nKey: 'i18nKey',
+ defaultsKey: 'defaults',
+ extensions: ['.js', '.jsx'], // not .ts or .tsx since we use i18next-scanner-typescript!
+ fallbackKey: (ns, value) => {return value},
+
+ // https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
+ supportBasicHtmlNodes: true, // Enables keeping the name of simple nodes (e.g. ) in translations instead of indexed keys.
+ keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of .
+
+ // // https://github.com/acornjs/acorn/tree/master/acorn#interface
+ // acorn: {
+ // ecmaVersion: 2020,
+ // sourceType: 'module', // defaults to 'module'
+ // }
+ },
+ lngs: ['en','de'],
+ ns: [],
+ defaultLng: 'en',
+ defaultNs: 'translation',
+ defaultValue: (lng, ns, key) => {
+ if (lng === 'en') {
+ return key; // Use key as value for base language
+ }
+ return ''; // Return empty string for other languages
+ },
+ resource: {
+ loadPath: './{{lng}}/{{ns}}.json',
+ savePath: './{{lng}}/{{ns}}.json',
+ jsonIndent: 2,
+ lineEnding: '\n'
+ },
+ nsSeparator: false, // namespace separator
+ keySeparator: false, // key separator
+ plurals: false,
+ interpolation: {
+ prefix: '{{',
+ suffix: '}}'
+ },
+ metadata: {},
+ allowDynamicKeys: false,
+ },
+
+ transform: typescriptTransform(
+ // options
+ {
+ // default value for extensions
+ extensions: [".ts", ".tsx"],
+ // optional ts configuration
+ tsOptions: {
+ target: "es2017",
+ },
+ },
+
+ function(outputText, file, enc, done) {
+ 'use strict';
+ const parser = this.parser;
+
+ parser.parseTransFromString(outputText);
+ parser.parseFuncFromString(outputText);
+
+ // const content = fs.readFileSync(file.path, enc);
+ // let count = 0;
+
+ // parser.parseFuncFromString(content, { list: ['i18n._', 'i18n.__'] }, (key, options) => {
+ // parser.set(key, Object.assign({}, options, {
+ // nsSeparator: false,
+ // keySeparator: false
+ // }));
+ // ++count;
+ // });
+
+ // if (count > 0) {
+ // console.log(`[i18next-scanner] transform: count=${chalk.cyan(count)}, file=${chalk.yellow(JSON.stringify(file.relative))}`);
+ // }
+
+ done();
+ }
+ ),
+
+};
diff --git a/client/public/locales/de/translation.json b/client/public/locales/de/translation.json
new file mode 100644
index 00000000..e7497d61
--- /dev/null
+++ b/client/public/locales/de/translation.json
@@ -0,0 +1,11 @@
+{
+ "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",
+ "levels": "Levels",
+ "tactics": "Taktiken",
+ "regular": "regulär",
+ "relaxed": "relaxed",
+ "none": "keine",
+ "Rules": "Regend"
+}
diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json
new file mode 100644
index 00000000..d4e17215
--- /dev/null
+++ b/client/public/locales/en/translation.json
@@ -0,0 +1,11 @@
+{
+ "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>
+
{/* Add more buttons for other languages as needed */}
diff --git a/client/src/components/popup/rules_help.tsx b/client/src/components/popup/rules_help.tsx
index e4ed1cd4..ca85386e 100644
--- a/client/src/components/popup/rules_help.tsx
+++ b/client/src/components/popup/rules_help.tsx
@@ -2,6 +2,7 @@
* @fileOverview
*/
import * as React from 'react'
+import { Trans, useTranslation } from 'react-i18next'
/** Pop-up that is displayed when opening the help explaining the game rules.
*
@@ -9,42 +10,46 @@ import * as React from 'react'
* controlled by the containing element.
*/
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
+ const { t, i18n } = useTranslation()
+
return
-
Game Rules
-
- 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.
-
-
- 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 simp in a level, you can use it henceforth in any level.
-
-
The options are:
+
{t("Game Rules")}
+
+
+ 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.
+
+
+ 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 simp in a level, you can use it henceforth in any level.
+
+
The options are:
+
-
levels
-
tactics
+
{t("levels")}
+
{t("tactics")}
-
regular
+
{t("regular")}
🔐
🔐
-
relaxed
+
{t("relaxed")}
🔓
🔐
-
none
+
{t("none")}
🔓
🔓
diff --git a/client/src/components/world_tree.tsx b/client/src/components/world_tree.tsx
index 65fcc8ac..9fbe1004 100644
--- a/client/src/components/world_tree.tsx
+++ b/client/src/components/world_tree.tsx
@@ -17,6 +17,7 @@ import { store } from '../state/store'
import '../css/world_tree.css'
import { PreferencesContext } from './infoview/context'
+import { useTranslation } from 'react-i18next'
// Settings for the world tree
cytoscape.use( klay )
@@ -195,6 +196,7 @@ export const downloadFile = ({ data, fileName, fileType } :
/** The menu that is shown next to the world selection graph */
export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
+ const { t, i18n } = useTranslation()
const gameId = React.useContext(GameIdContext)
const difficulty = useSelector(selectDifficulty(gameId))
const dispatch = useAppDispatch()
@@ -202,20 +204,20 @@ export function WorldSelectionMenu({rulesHelp, setRulesHelp}) {
function label(x : number) {
- return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
+ return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
}
return