Add i18n, routing and dark-mode support

Introduce internationalization and basic routing: add i18n initialization (src/i18n.js), translation files (public/i18n/en.json, de.json), and wire i18n into the app (import in main.jsx). Update App.jsx to use react-router (BrowserRouter, Routes) and react-i18next (useTranslation), provide simple Landing/Login/Imprint pages, a language toggle and a dark-mode toggle. Add dependencies in package.json (i18next, i18next-http-backend, react-i18next, react-router-dom) and small Tailwind style tweak (custom-variant for dark) in src/style.css.
This commit is contained in:
lobo committed 2026-03-28 09:29:39 +01:00
1 parent 7ff53c0f6c
commit c8f1d66cd2
8 files changed
+343 -64

No files matched your search

+219 -4
View File
@@ -10,8 +10,12 @@
"dependencies": {
"@mdi/font": "~7.4.47",
"@tailwindcss/vite": "~4.2.2",
"i18next": "~26.0.0",
"i18next-http-backend": "~3.0.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-i18next": "~17.0.0",
"react-router-dom": "~7.13.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.1",
@@ -21,6 +25,15 @@
"vite": "^8.0.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
@@ -773,6 +786,28 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -863,6 +898,55 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.0.tgz",
"integrity": "sha512-+Dg27j7VH40WRy+Q010hj3jxlxDb3qFkA7EwuK4kx0+glLxEqZsxdB8MqliPBkTrXEuNlEfeosM3lucAHK+0tQ==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2"
},
"peerDependencies": {
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -1148,6 +1232,26 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
@@ -1162,9 +1266,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -1229,6 +1333,71 @@
"react": "^19.2.4"
}
},
"node_modules/react-i18next": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.0.tgz",
"integrity": "sha512-L7aqwOePCExt6nlF7000lN2YKWnR7IpSpQId9sj01798Xn3LAncBdTHKl9lA/nr+YrG78BTqWPJxq9mlrrmH7Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.10.10",
"react": ">= 16.8.0",
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-router": {
"version": "7.13.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz",
"integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz",
"integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.10",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz",
@@ -1274,6 +1443,12 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1318,6 +1493,12 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1356,6 +1537,15 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz",
@@ -1432,6 +1622,31 @@
"optional": true
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}
+5 -1
View File
@@ -11,8 +11,12 @@
"dependencies": {
"@mdi/font": "~7.4.47",
"@tailwindcss/vite": "~4.2.2",
"i18next": "~26.0.0",
"i18next-http-backend": "~3.0.2",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-i18next": "~17.0.0",
"react-router-dom": "~7.13.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.1",
+5
View File
@@ -0,0 +1,5 @@
{
"welcome": "Dein Game-Backlog unter Kontrolle",
"login": "Anmelden",
"imprint": "Impressum"
}
+5
View File
@@ -0,0 +1,5 @@
{
"welcome": "Control your game backlog",
"login": "Login",
"imprint": "Imprint"
}
+87 -59
View File
@@ -1,67 +1,95 @@
import React from 'react';
function App() {
// Array to generate the placeholder game cards
const mockGames = [1, 2, 3];
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useState, useEffect } from 'react';
// Placeholder components to keep the routing structure simple
function LandingPage() {
const { t } = useTranslation();
return (
<div className="min-h-screen bg-slate-50 flex flex-col">
<header className="border-b shadow-sm bg-white p-4 flex justify-between items-center">
<div className="font-bold text-blue-600 flex items-center">
<i className="mdi mdi-controller mr-2 text-xl"></i>
To Be Played
</div>
<div>
<button className="mr-4">Dashboard</button>
<button className="bg-blue-600 text-white px-4 py-2 rounded">Login</button>
</div>
</header>
<main className="grow container mx-auto py-20 px-4">
<section className="text-center mb-16">
<h1 className="text-4xl md:text-5xl font-extrabold text-slate-900 mb-4">
Dein Game-Backlog unter Kontrolle.
<div className="grow flex items-center justify-center p-4">
<h1 className="text-3xl md:text-5xl font-bold text-slate-900 dark:text-white">
{t('welcome')}
</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Willkommen bei deiner neuen Applikation. Hier werden bald deine Spiele aus dem
<code className="bg-slate-200 px-1 rounded text-blue-600 ml-1">game-service</code> gelistet.
</p>
</section>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{mockGames.map((n) => {
return (
<div key={n} className="border border-slate-200 rounded-xl bg-white overflow-hidden">
<div className="h-48 bg-slate-100">
<img
src={`https://picsum.photos/500/300?random=${n}`}
alt={`Beispiel Spiel ${n}`}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h3 className="font-bold text-lg">Beispiel Spiel {n}</h3>
<p className="text-sm text-slate-500 mb-2">Status: In Warteschlange</p>
<p className="text-slate-500 text-sm">
Dies ist ein Platzhalter für ein Spiel in deiner Liste. Später kommen hier echte Daten per API.
</p>
</div>
<div className="border-t p-4">
<button className="w-full bg-blue-100 text-blue-700 py-2 rounded font-medium hover:bg-blue-200">
Details ansehen
</button>
</div>
</div>
);
})}
</div>
</main>
<footer className="bg-white border-t py-4 text-center text-slate-400 text-sm">
&copy; 2026 To Be Played — Entwickelt mit React & Tailwind v4
</footer>
</div>
);
}
function LoginPage() {
const { t } = useTranslation();
return (
<div className="grow flex items-center justify-center p-4">
<h2 className="text-2xl text-slate-800 dark:text-slate-200">{t('login')}</h2>
</div>
);
}
function ImprintPage() {
const { t } = useTranslation();
return (
<div className="grow p-8">
<h2 className="text-2xl text-slate-800 dark:text-slate-200">{t('imprint')}</h2>
</div>
);
}
function App() {
const { t, i18n } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(true);
// Apply dark mode class to the root HTML element for Tailwind
useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDarkMode]);
const toggleLanguage = () => {
if (i18n.language === 'de') {
i18n.changeLanguage('en').then(r => console.log("Changed language to en"));
} else {
i18n.changeLanguage('de').then(r => console.log("Changed language to en"));
}
};
return (
<BrowserRouter>
<div className="min-h-screen flex flex-col bg-slate-50 dark:bg-slate-950 transition-colors duration-300">
<header className="p-4 flex justify-between items-center border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
<Link to="/" className="font-bold text-blue-600 dark:text-blue-400 text-xl flex items-center">
<i className="mdi mdi-controller mr-2"></i> To Be Played
</Link>
<div className="flex gap-4 items-center">
<button onClick={toggleLanguage} className="text-sm font-medium dark:text-slate-300">
{i18n.language.toUpperCase()}
</button>
<button onClick={() => { setIsDarkMode(!isDarkMode); }} className="text-xl dark:text-slate-300">
<i className={isDarkMode ? "mdi mdi-weather-sunny" : "mdi mdi-weather-night"}></i>
</button>
<Link to="/login" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors">
{t('login')}
</Link>
</div>
</header>
<main className="flex-grow flex flex-col">
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/impressum" element={<ImprintPage />} />
</Routes>
</main>
<footer className="p-4 text-center border-t border-slate-200 dark:border-slate-800 text-slate-500 text-sm bg-white dark:bg-slate-900">
<Link to="/impressum" className="hover:text-blue-500 transition-colors">
{t('imprint')}
</Link>
</footer>
</div>
</BrowserRouter>
);
}
export default App;
+19
View File
@@ -0,0 +1,19 @@
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
i18n.use(HttpBackend)
.use(initReactI18next)
.init({
lng: 'de',
fallbackLng: 'en',
backend: {
loadPath: '/i18n/{{lng}}.json',
},
interpolation: {
escapeValue: false
}
});
export default i18n;
+1
View File
@@ -1,6 +1,7 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './i18n';
import './style.css';
import '@mdi/font/css/materialdesignicons.css';
+2
View File
@@ -1 +1,3 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));