Skip to content

Commit

Permalink
implement i18next and i18next-scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
joneugster committed Mar 24, 2024
1 parent c24efb1 commit 45bc046
Show file tree
Hide file tree
Showing 9 changed files with 780 additions and 42 deletions.
101 changes: 101 additions & 0 deletions client/i18next-scanner.config.cjs
Original file line number Diff line number Diff line change
@@ -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. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'], // Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.

// // 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();
}
),

};
11 changes: 11 additions & 0 deletions client/public/locales/de/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Lean Game Server": "Lean-Spieleserver",
"<p>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.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>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.</p><1>Bemerkung: \"Freigeschaltete\" Taktiken (und Theoreme) werden durch zwei Faktoren bestimmt: The Menge der Taktiken die minimal notwending sind um den Level zu lösen und dazu die Menge aller Taktiken, die in einem anderen Level freigeschaltet wurden. Das bedeutet wenn <1>simp</1> 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"
}
11 changes: 11 additions & 0 deletions client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Lean Game Server": "Lean Game Server",
"<p>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.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>": "<p>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.</p><1>Note: \"Unlocked\" tactics (or theorems) are determined by two things: The set of minimal tactics needed to solve a level, plus any tactics you unlocked in another level. That means if you unlock <1>simp</1> in a level, you can use it henceforth in any level.</1><p>The options are:</p>",
"Game Rules": "Game Rules",
"levels": "levels",
"tactics": "tactics",
"regular": "regular",
"relaxed": "relaxed",
"none": "none",
"Rules": "Rules"
}
3 changes: 2 additions & 1 deletion client/src/components/landing_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ function LandingPage() {
"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">
<header style={{backgroundImage: `url(${bgImage})`}}>
Expand All @@ -149,6 +149,7 @@ function LandingPage() {
<div>
<button onClick={() => i18n.changeLanguage("en")}>{flag["English"]}</button>
<button onClick={() => i18n.changeLanguage("fr")}>{flag["French"]}</button>
<button onClick={() => i18n.changeLanguage("de")}>{flag["German"]}</button>
{/* Add more buttons for other languages as needed */}
</div>
<div id="main-title">
Expand Down
37 changes: 21 additions & 16 deletions client/src/components/popup/rules_help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,54 @@
* @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.
*
* `handleClose` is the function to close it again because it's open/closed state is
* controlled by the containing element.
*/
export function RulesHelpPopup ({handleClose}: {handleClose: () => void}) {
const { t, i18n } = useTranslation()

return <div className="privacy-policy modal-wrapper">
<div className="modal-backdrop" onClick={handleClose} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h2>Game Rules</h2>
<p>
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.
</p>
<p>
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 <code>simp</code> in a level, you can use it henceforth in any level.
</p>
<p>The options are:</p>
<h2>{t("Game Rules")}</h2>
<Trans>
<p>
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.
</p>
<p>
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 <code>simp</code> in a level, you can use it henceforth in any level.
</p>
<p>The options are:</p>
</Trans>
<table>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">levels</th>
<th scope="col">tactics</th>
<th scope="col">{t("levels")}</th>
<th scope="col">{t("tactics")}</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">regular</th>
<th scope="row">{t("regular")}</th>
<td>🔐</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">relaxed</th>
<th scope="row">{t("relaxed")}</th>
<td>🔓</td>
<td>🔐</td>
</tr>
<tr>
<th scope="row">none</th>
<th scope="row">{t("none")}</th>
<td>🔓</td>
<td>🔓</td>
</tr>
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/world_tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down Expand Up @@ -195,27 +196,28 @@ 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()
const { mobile } = React.useContext(PreferencesContext)


function label(x : number) {
return x == 0 ? 'none' : x == 1 ? 'relaxed' : 'regular'
return x == 0 ? t("none") : x == 1 ? t("relaxed") : t("regular")
}


return <nav className={`world-selection-menu${mobile ? '' : ' desktop'}`}>
<div className="slider-wrap">
<span className="difficulty-label">Rules
<span className="difficulty-label">{t("Rules")}
<FontAwesomeIcon icon={rulesHelp ? faXmark : faCircleQuestion} className='helpButton' onClick={() => (setRulesHelp(!rulesHelp))}/>
</span>
<Slider
orientation="vertical"
title="Game Rules"
title={t("Game Rules")}
min={0} max={2}
aria-label="Game Rules"
aria-label={t("Game Rules")}
value={difficulty}
marks={[
{value: 0, label: label(0)},
Expand Down
22 changes: 2 additions & 20 deletions client/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
import i18n from "i18next";
import Backend from "i18next-http-backend"
import { initReactI18next } from "react-i18next";

// the translations
// (tip move them in a JSON file and import them,
// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files)
const resources = {
en: {
translation: {
"Welcome to React": "Welcome to React and react-i18next",
"Lean Game Server": "Lean Game Server translated"

}
},
fr: {
translation: {
"Welcome to React": "Bienvenue à React et react-i18next",
"Lean Game Server": "Lean Game Server French"
}
}
};

i18n
.use(initReactI18next) // passes i18n down to react-i18next
.use(Backend)
.init({
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
// if you're using a language detector, do not define the lng option
Expand Down
Loading

0 comments on commit 45bc046

Please sign in to comment.