webui: Add i18n

This commit is contained in:
Kevin Kandlbinder 2022-03-06 01:31:02 +01:00
parent ac5faca859
commit 9e05c54d81
30 changed files with 1098 additions and 112 deletions

View file

@ -0,0 +1,17 @@
module.exports = {
createOldCatalogs: true,
lexers: {
js: ['JsxLexer'],
default: ['JavascriptLexer'],
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
},
skipDefaultValues: (locale, ns) => {return locale !== "en"},
locales: ['en', 'de'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
input: [
'src/**/*.tsx',
'src/*.tsx',
],
}

View file

@ -12,11 +12,18 @@
"@types/node": "^12.20.46", "@types/node": "^12.20.46",
"@types/react": "^16.14.23", "@types/react": "^16.14.23",
"@types/react-dom": "^16.9.14", "@types/react-dom": "^16.9.14",
"@types/react-helmet": "^6.1.5",
"@types/react-redux": "^7.1.22", "@types/react-redux": "^7.1.22",
"axios": "^0.26.0", "axios": "^0.26.0",
"i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"i18next-parser": "^5.4.0",
"lucide-react": "^0.17.6", "lucide-react": "^0.17.6",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-i18next": "^11.15.5",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-router-dom": "6", "react-router-dom": "6",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
@ -27,7 +34,8 @@
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"extract-translations": "i18next --fail-on-warnings"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -7,37 +7,14 @@
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Protector of your Matrix-Harvest!"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/> <link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/> <link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!-- <title>Matrix-Veles WebUI</title>
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Redux App</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View file

@ -0,0 +1,25 @@
{
"login": {
"htmlTitle": "Einloggen bei Veles",
"title": "Einloggen",
"logging_in": "Logge ein...",
"login": "Einloggen",
"register_instead": "Ich habe noch keinen Account"
},
"username": "Benutzername",
"password": "Passwort",
"register": {
"register": "Registrieren",
"htmlTitle": "Registrieren bei Veles",
"title": "Registrieren",
"login_instead": "Ich habe schon einen Account"
},
"matrix_handle": "Matrix-Name",
"selector": {
"question": "Kennen wir uns?",
"login": "Jup, ich log mich ein",
"register": "Nee, ich registrier' mich"
},
"help": "Hilfe",
"source": "Quellcode"
}

View file

@ -0,0 +1,12 @@
{
"jump_to_content": "Zum Inhalt springen",
"jump_to_navigation": "Zur Navigation springen",
"documentation": "Dokumentation",
"navigation": {
"dashboard": "Dashboard",
"rooms": "Meine Räume",
"hash_checker": "Hash-Prüfer",
"hash_lists": "Listen",
"hash_entries": "Einträge"
}
}

View file

@ -0,0 +1,3 @@
{
"test": "Tst2"
}

View file

@ -0,0 +1,25 @@
{
"login": {
"htmlTitle": "Login to Veles",
"title": "Login",
"logging_in": "Logging in...",
"login": "Login",
"register_instead": "I don't have an account"
},
"username": "Username",
"password": "Password",
"register": {
"register": "Register",
"htmlTitle": "Register with Veles",
"title": "Register",
"login_instead": "I already have an account"
},
"matrix_handle": "Matrix-Handle",
"selector": {
"question": "Do we know each other?",
"login": "Yeah, let me log in",
"register": "Nah, I'll sign up"
},
"help": "Help",
"source": "Source Code"
}

View file

@ -0,0 +1,12 @@
{
"jump_to_content": "Jump to Content",
"jump_to_navigation": "Jump to Navigation",
"documentation": "Documentation",
"navigation": {
"dashboard": "Dashboard",
"rooms": "My Rooms",
"hash_checker": "Hash-Checker",
"hash_lists": "Lists",
"hash_entries": "Entries"
}
}

View file

@ -0,0 +1,3 @@
{
"test": "Test"
}

29
webui/public/logo.svg Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="120" height="120" version="1.1" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient43644">
<stop stop-color="#007300" offset="0"/>
<stop stop-color="#007300" stop-opacity=".0072181" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient1301" x1="49.693" x2="69.839" y1="25.512" y2="25.512" gradientTransform="translate(4,8)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
<linearGradient id="linearGradient1303" x1="49.777" x2="70.07" y1="81.546" y2="81.546" gradientTransform="translate(0,6)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
<linearGradient id="linearGradient31669" x1="91.453" x2="72.905" y1="48.722" y2="48.722" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient43644"/>
</defs>
<g transform="translate(-3.6704 12.24)" fill="url(#linearGradient31669)" style="shape-inside:url(#rect8261);white-space:pre" aria-label="101 011 110 100">
<path d="m74.758 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 2.1413q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m108.69 17.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m79.928 27.475q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m91.725 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m108.69 42.969h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m74.758 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m91.725 68.303h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m113.86 52.808q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m74.758 93.636h4.2969v-14.831l-4.6745 0.9375v-2.3958l4.6484-0.9375h2.6302v17.227h4.2969v2.2135h-11.198z"/>
<path d="m96.894 78.141q-2.0313 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781-3.2682 0-5-2.5781-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
<path d="m113.86 78.141q-2.0312 0-3.0599 2.0052-1.0156 1.9922-1.0156 6.0026 0 3.9974 1.0156 6.0026 1.0286 1.9922 3.0599 1.9922 2.0443 0 3.0599-1.9922 1.0286-2.0052 1.0286-6.0026 0-4.0104-1.0286-6.0026-1.0156-2.0052-3.0599-2.0052zm0-2.0833q3.2682 0 4.987 2.5911 1.7318 2.5781 1.7318 7.5 0 4.9089-1.7318 7.5-1.7188 2.5781-4.987 2.5781t-5-2.5781q-1.7188-2.5911-1.7188-7.5 0-4.9219 1.7188-7.5 1.7318-2.5911 5-2.5911z"/>
</g>
<path d="m4.0226 8.0649 30.026 50.894h59.933l30.009-50.864" fill="none" stroke="url(#linearGradient1301)" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.048"/>
<path d="m36.324 67.382h47.484l-23.793 40.329z" fill="none" stroke="url(#linearGradient1303)" stroke-linejoin="round" stroke-width="4.048"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,19 +1,29 @@
{ {
"short_name": "React App", "short_name": "Veles",
"name": "Create React App Sample", "name": "Matrix-Veles",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "android-chrome-512x512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }

View file

@ -8,12 +8,16 @@ import {useAppDispatch} from "./app/hooks";
import broadcastChannel from "./app/broadcastChannel"; import broadcastChannel from "./app/broadcastChannel";
import {logOut, receiveAuthUpdate} from "./features/auth/authSlice"; import {logOut, receiveAuthUpdate} from "./features/auth/authSlice";
import PanelLayout from "./layouts/PanelLayout"; import PanelLayout from "./layouts/PanelLayout";
import {Trans, useTranslation} from "react-i18next";
function App() { function App() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
// This needs to be here to prevent a weird bug
useTranslation()
broadcastChannel.on("message", (ev) => { broadcastChannel.on("message", (ev) => {
if(ev.action == "updateAuth") { if(ev.action === "updateAuth") {
dispatch(receiveAuthUpdate(ev)) dispatch(receiveAuthUpdate(ev))
} }
}) })
@ -25,10 +29,11 @@ function App() {
<Route path={"register"} element={<RegisterView/>} /> <Route path={"register"} element={<RegisterView/>} />
</Route> </Route>
<Route path={"/"} element={<PanelLayout/>}> <Route path={"/"} element={<PanelLayout/>}>
<Route path={""} element={<RequireAuth><h1>hi</h1> <button onClick={() => { <Route path={""} element={<RequireAuth><h1><Trans i18nKey={"test"}>Test</Trans></h1> <button onClick={() => {
dispatch(logOut()) dispatch(logOut())
} }
}>Log out</button></RequireAuth>} /> }>Log out</button></RequireAuth>} />
<Route path={"rooms"} element={<RequireAuth><h1>rooms</h1></RequireAuth>} />
<Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>} /> <Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>} />
<Route path={"hashing/entries"} element={<RequireAuth><h1>entries</h1></RequireAuth>} /> <Route path={"hashing/entries"} element={<RequireAuth><h1>entries</h1></RequireAuth>} />
</Route> </Route>

View file

@ -1,16 +1,18 @@
import React, {useRef, useState} from "react"; import React, {useState} from "react";
import styles from "./AuthViews.module.scss"; import styles from "./AuthViews.module.scss";
import {ReactComponent as Logo} from "../../logo.svg"; import {ReactComponent as Logo} from "../../logo.svg";
import {Link, useLocation, useNavigate} from "react-router-dom"; import {Link, useLocation} from "react-router-dom";
import {axiosDefault} from "../../context/axios"; import {axiosDefault} from "../../context/axios";
import {AxiosError} from "axios"; import {AxiosError} from "axios";
import {useAppDispatch, useAppSelector} from "../../app/hooks"; import {useAppDispatch} from "../../app/hooks";
import {logIn, selectAuth} from "../../features/auth/authSlice"; import {logIn} from "../../features/auth/authSlice";
import {Key} from "lucide-react"; import {Key} from "lucide-react";
import {AuthLocationState} from "../../layouts/AuthLayout"; import {AuthLocationState} from "../../layouts/AuthLayout";
import {Helmet} from "react-helmet";
import {Trans, useTranslation} from "react-i18next";
const LoginView = () => { const LoginView = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -25,6 +27,8 @@ const LoginView = () => {
const locationState = location.state as AuthLocationState const locationState = location.state as AuthLocationState
const {t} = useTranslation()
const onSubmit = async () => { const onSubmit = async () => {
setLoading(true) setLoading(true)
setError("") setError("")
@ -42,7 +46,7 @@ const LoginView = () => {
if((e as AxiosError).isAxiosError) { if((e as AxiosError).isAxiosError) {
const axErr = e as AxiosError const axErr = e as AxiosError
setError("Server returned error: "+axErr.response?.data.error) setError(": "+axErr.response?.data.error)
} else { } else {
setError("An unknown error occurred.") setError("An unknown error occurred.")
} }
@ -53,23 +57,27 @@ const LoginView = () => {
return <> return <>
<Logo width={64} height={64} /> <Logo width={64} height={64} />
<h1>Login</h1> <Helmet>
<title>{t("auth:login.htmlTitle", "Login to Veles")}</title>
</Helmet>
<h1><Trans i18nKey={"auth:login.title"}>Login</Trans></h1>
{ loading && <div className={styles.loader}> { loading && <div className={styles.loader}>
<Key/> <Key/>
<span>Logging in...</span> <span><Trans i18nKey={"auth:login.logging_in"}>Logging in...</Trans></span>
</div>} </div>}
{ !loading && <> { !loading && <>
{error !== "" && <span className={styles.error}>{error}</span>} {error !== "" && <span className={styles.error}>{error}</span>}
<form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}> <form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={"Username"} /> <input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={t("auth:username", "Username")} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={"Password"} type={"password"} /> <input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={t("auth:password", "Password")} type={"password"} />
<button onClick={() => onSubmit()}>Login</button> <button onClick={() => onSubmit()}><Trans i18nKey={"auth:login.login"}>Login</Trans></button>
</form> </form>
<Link to={"/auth/register"} className={styles.mindChangedLink} aria-label={"Register"} state={locationState}>I don't have an account</Link> <Link to={"/auth/register"} className={styles.mindChangedLink} aria-label={t("auth:register.register", "Register")} state={locationState}><Trans i18nKey={"auth:login.register_instead"}>I don't have an account</Trans></Link>
</>} </>}
</> </>
} }

View file

@ -1,12 +1,13 @@
import React, {useRef, useState} from "react"; import React, {useState} from "react";
import styles from "./AuthViews.module.scss"; import styles from "./AuthViews.module.scss";
import {ReactComponent as Logo} from "../../logo.svg"; import {ReactComponent as Logo} from "../../logo.svg";
import {Link, useLocation, useNavigate} from "react-router-dom"; import {Link, useLocation} from "react-router-dom";
import {AuthLocationState} from "../../layouts/AuthLayout"; import {AuthLocationState} from "../../layouts/AuthLayout";
import {useAppDispatch, useAppSelector} from "../../app/hooks"; import {useAppDispatch} from "../../app/hooks";
import {selectAuth} from "../../features/auth/authSlice"; import {Helmet} from "react-helmet";
import {Trans, useTranslation} from "react-i18next";
const RegisterView = () => { const RegisterView = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -19,22 +20,28 @@ const RegisterView = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const {t} = useTranslation()
const onSubmit = () => { const onSubmit = () => {
console.log(username, password) console.log(username, password, matrix)
} }
return <> return <>
<Logo width={64} height={64} /> <Logo width={64} height={64} />
<h1>Register</h1> <Helmet>
<title>{t("auth:register.htmlTitle", "Register with Veles")}</title>
</Helmet>
<h1><Trans i18nKey={"auth:register.title"}>Register</Trans></h1>
<form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}> <form onSubmit={(e) => {e.preventDefault(); onSubmit()}} className={styles.authForm}>
<input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={"Username"} autoCapitalize={"no"} autoCorrect={"no"} /> <input onChange={(ev) => setUsername(ev.target.value)} value={username} placeholder={t("auth:username", "Username")} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={"Password"} type={"password"} autoCapitalize={"no"} autoCorrect={"no"} /> <input onChange={(ev) => setPassword(ev.target.value)} value={password} placeholder={t("auth:password", "Password")} type={"password"} autoCapitalize={"no"} autoCorrect={"no"} />
<input onChange={(ev) => setMatrix(ev.target.value)} value={matrix} placeholder={"Matrix-Handle (@user:matrix.org)"} autoCapitalize={"no"} autoCorrect={"no"} /> <input onChange={(ev) => setMatrix(ev.target.value)} value={matrix} placeholder={t("auth:matrix_handle", "Matrix-Handle")+" (@user:matrix.org)"} autoCapitalize={"no"} autoCorrect={"no"} />
<button onClick={() => onSubmit()}>Register</button> <button onClick={() => onSubmit()}><Trans i18nKey={"auth:register.register"}>Register</Trans></button>
</form> </form>
<Link to={"/auth/login"} className={styles.mindChangedLink} aria-label={"Register"} state={locationState}>I already have an account</Link> <Link to={"/auth/login"} className={styles.mindChangedLink} aria-label={t("auth:login.login", "Login")} state={locationState}><Trans i18nKey={"auth:register.login_instead"}>I already have an account</Trans></Link>
</> </>
} }

View file

@ -1,4 +1,4 @@
import React, {ReactNode} from "react"; import React from "react";
import {useAppSelector} from "../../app/hooks"; import {useAppSelector} from "../../app/hooks";
import {selectAuth} from "./authSlice"; import {selectAuth} from "./authSlice";
import {useLocation} from "react-router-dom"; import {useLocation} from "react-router-dom";

29
webui/src/i18n.ts Normal file
View file

@ -0,0 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
ns: ["translation", "auth", "panel"],
supportedLngs: ["en", "de"],
fallbackLng: "en",
interpolation: {
escapeValue: false
},
detection: {
lookupCookie: "velesLng",
lookupQuerystring: "lang",
lookupLocalStorage: "velesLng",
lookupSessionStorage: "velesLng"
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
}
})
export default i18n

View file

@ -26,6 +26,7 @@ html, body, #root {
--veles-layout-padding: 20px; --veles-layout-padding: 20px;
--veles-layout-padding-slim: 10px; --veles-layout-padding-slim: 10px;
--veles-layout-padding-wide: 40px;
--veles-layout-border-radius: 10px; --veles-layout-border-radius: 10px;
} }

View file

@ -7,12 +7,16 @@ import {Provider} from 'react-redux';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import "./i18n";
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<React.Suspense fallback={<h1>Loading...</h1>}>
<BrowserRouter> <BrowserRouter>
<App/> <App/>
</BrowserRouter> </BrowserRouter>
</React.Suspense>
</Provider> </Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')

View file

@ -7,6 +7,7 @@ import {UserPlus, User} from "lucide-react";
import {ReactComponent as Logo} from "../logo.svg"; import {ReactComponent as Logo} from "../logo.svg";
import {useAppSelector} from "../app/hooks"; import {useAppSelector} from "../app/hooks";
import {selectAuth} from "../features/auth/authSlice"; import {selectAuth} from "../features/auth/authSlice";
import {Trans} from "react-i18next";
export type AuthLocationState = { export type AuthLocationState = {
location?: Location location?: Location
@ -23,13 +24,13 @@ const AuthLayout = () => {
useEffect(() => { useEffect(() => {
if(authState.status == "logged_in") { if(authState.status === "logged_in") {
if(locationState && locationState.location) { if(locationState && locationState.location) {
navigate(locationState.location, {replace: true}) navigate(locationState.location, {replace: true})
} }
navigate("/", {replace: true}) navigate("/", {replace: true})
} }
}, [authState]) }, [authState, locationState, navigate])
return <div className={styles.auth}> return <div className={styles.auth}>
<div className={styles.background}/> <div className={styles.background}/>
@ -38,21 +39,21 @@ const AuthLayout = () => {
{outlet || <> {outlet || <>
<Logo width={64} height={64} /> <Logo width={64} height={64} />
<h1>Matrix-Veles</h1> <h1>Matrix-Veles</h1>
<h2>Do we know each other?</h2> <h2><Trans i18nKey={"auth:selector.question"}>Do we know each other?</Trans></h2>
<div className={styles.splitChoice}> <div className={styles.splitChoice}>
<Link to={"./login"} state={locationState}> <Link to={"./login"} state={locationState}>
<User/> <User/>
<span>Yeah, let me log in</span> <span><Trans i18nKey={"auth:selector.login"}>Yeah, let me log in</Trans></span>
</Link> </Link>
<Link to={"./register"} state={locationState}> <Link to={"./register"} state={locationState}>
<UserPlus/> <UserPlus/>
<span>Nah, I'll sign up</span> <span><Trans i18nKey={"auth:selector.register"}>Nah, I'll sign up</Trans></span>
</Link> </Link>
</div> </div>
</>} </>}
</div> </div>
<footer>Veles WebUI | <a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}>Help</a> | <a href={"https://github.com/Unkn0wnCat/matrix-veles"} target={"_blank"} rel={"noreferrer"}>Source Code</a></footer> <footer>Veles WebUI | <a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><Trans i18nKey={"auth:help"}>Help</Trans></a> | <a href={"https://github.com/Unkn0wnCat/matrix-veles"} target={"_blank"} rel={"noreferrer"}><Trans i18nKey={"auth:source"}>Source Code</Trans></a></footer>
</div> </div>
</div> </div>
} }

View file

@ -24,8 +24,17 @@
transition: color .25s; transition: color .25s;
background: transparent;
color: inherit;
font: inherit;
border: none;
font-weight: 600; font-weight: 600;
cursor: pointer;
height: 65px;
&:hover, &.active, &:focus { &:hover, &.active, &:focus {
color: var(--veles-color-accent); color: var(--veles-color-accent);
} }
@ -46,10 +55,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
//overflow: hidden;
.topBar { .topBar {
display: flex; display: flex;
border-bottom: thin solid var(--veles-color-border); border-bottom: thin solid var(--veles-color-border);
height: 66px;
a { a {
@include panelTopBarLink; @include panelTopBarLink;
@ -74,11 +85,55 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: thin solid var(--veles-color-border); border-right: thin solid var(--veles-color-border);
overflow-x: auto;
height: calc(100vh - 66px);
width: 250px;
a { >* {
flex-shrink: 0;
}
.dropdown {
height: 65px;
overflow: hidden;
transition: height .25s;
> button > svg {
transition: transform .25s;
}
> a {
display: none;
padding-left: var(--veles-layout-padding-wide)
}
&.expanded {
height: unset;
> a {
display: flex;
}
> button > svg {
transform: rotate(90deg);
}
}
}
a, button {
@include panelTopBarLink; @include panelTopBarLink;
width: 100%;
padding: var(--veles-layout-padding); padding: var(--veles-layout-padding);
} }
button {
&:focus {
color: white;
}
&:active, &:hover {
color: var(--veles-color-accent);
}
}
} }
> main { > main {

View file

@ -1,27 +1,35 @@
import React from "react"; import React, {useState} from "react";
import {Link, NavLink, useOutlet} from "react-router-dom"; import {Link, NavLink, useOutlet} from "react-router-dom";
import {Home, List, ClipboardList, ExternalLink} from "lucide-react"; import {Home, List, ClipboardList, ExternalLink, ChevronRight, MessageSquare} from "lucide-react";
import {ReactComponent as Logo} from "../logo.svg"; import {ReactComponent as Logo} from "../logo.svg";
import styles from "./PanelLayout.module.scss"; import styles from "./PanelLayout.module.scss";
import {Trans} from "react-i18next";
const PanelLayout = () => { const PanelLayout = () => {
const outlet = useOutlet(); const outlet = useOutlet();
const [hashingExpanded, setHashingExpanded] = useState(false)
return <div className={styles.panel}> return <div className={styles.panel}>
<a href={"#main"} className={styles.skipToContent}>Jump to Content</a> <a href={"#main"} className={styles.skipToContent}><Trans i18nKey={"panel:jump_to_content"}>Jump to Content</Trans></a>
<a href={"#navigation"} className={styles.skipToContent}>Jump to Navigation</a> <a href={"#navigation"} className={styles.skipToContent}><Trans i18nKey={"panel:jump_to_navigation"}>Jump to Navigation</Trans></a>
<div className={styles.topBar}> <div className={styles.topBar}>
<Link to={"/"} className={styles.logo}><Logo/> <span>Matrix-Veles</span></Link> <Link to={"/"} className={styles.logo}><Logo/> <span>Matrix-Veles</span></Link>
<a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><ExternalLink/> <span>Documentation</span></a> <a href={"https://veles.1in1.net/docs/intro"} target={"_blank"} rel={"noreferrer"}><ExternalLink/> <span><Trans i18nKey={"panel:documentation"}>Documentation</Trans></span></a>
</div> </div>
<div className={styles.content}> <div className={styles.content}>
<nav id={"navigation"}> <nav id={"navigation"}>
<NavLink to={"/"}><Home/><span>Dashboard</span></NavLink> <NavLink to={"/"}><Home/><span><Trans i18nKey={"panel:navigation.dashboard"}>Dashboard</Trans></span></NavLink>
<NavLink to={"/hashing/lists"}><List/><span>Lists</span></NavLink> <NavLink to={"/rooms"}><MessageSquare/><span><Trans i18nKey={"panel:navigation.rooms"}>My Rooms</Trans></span></NavLink>
<NavLink to={"/hashing/entries"}><ClipboardList/><span>Entries</span></NavLink> <div className={styles.dropdown + (hashingExpanded?" "+styles.expanded:"")}>
<button onClick={() => setHashingExpanded(!hashingExpanded)}>
<ChevronRight/> <span><Trans i18nKey={"panel:navigation.hash_checker"}>Hash-Checker</Trans></span>
</button>
<NavLink to={"/hashing/lists"}><List/><span><Trans i18nKey={"panel:navigation.hash_lists"}>Lists</Trans></span></NavLink>
<NavLink to={"/hashing/entries"}><ClipboardList/><span><Trans i18nKey={"panel:navigation.hash_entries"}>Entries</Trans></span></NavLink>
</div>
</nav> </nav>
<main id={"main"}> <main id={"main"}>
{outlet} {outlet}

File diff suppressed because it is too large Load diff