localization

This commit is contained in:
Roman 2022-11-30 23:15:52 +01:00
parent 1ba27e4f40
commit 3e3b7d7b2b
16 changed files with 176 additions and 155 deletions

@ -11,7 +11,7 @@ use Core\Objects\Router\Router;
class Account extends TemplateDocument { class Account extends TemplateDocument {
public function __construct(Router $router, string $templateName) { public function __construct(Router $router, string $templateName) {
parent::__construct($router, $templateName); parent::__construct($router, $templateName);
$this->languageModules = ["general", "account"]; $this->languageModules[] = "account";
$this->title = "Account"; $this->title = "Account";
$this->searchable = false; $this->searchable = false;
$this->enableCSP(); $this->enableCSP();

@ -34,7 +34,7 @@ abstract class Document {
$this->domain = $this->getSettings()->getBaseUrl(); $this->domain = $this->getSettings()->getBaseUrl();
$this->logger = new Logger("Document", $this->getSQL()); $this->logger = new Logger("Document", $this->getSQL());
$this->searchable = false; $this->searchable = false;
$this->languageModules = []; $this->languageModules = ["general"];
} }
public abstract function getTitle(): string; public abstract function getTitle(): string;

@ -1,8 +1,8 @@
<?php <?php
return [ return [
"title" => "Einloggen", "login_title" => "Einloggen",
"description" => "Loggen Sie sich in Ihren Account ein", "login_description" => "Loggen Sie sich in Ihren Account ein",
"form_title" => "Bitte geben Sie ihre Daten ein", "form_title" => "Bitte geben Sie ihre Daten ein",
"username" => "Benutzername", "username" => "Benutzername",
"username_or_email" => "Benutzername oder E-Mail", "username_or_email" => "Benutzername oder E-Mail",

@ -2,6 +2,7 @@
return [ return [
"something_went_wrong" => "Etwas ist schief gelaufen", "something_went_wrong" => "Etwas ist schief gelaufen",
"error_occurred" => "Ein Fehler ist aufgetreten",
"retry" => "Erneut versuchen", "retry" => "Erneut versuchen",
"Go back" => "Zurück", "Go back" => "Zurück",
"submitting" => "Übermittle", "submitting" => "Übermittle",

@ -2,6 +2,7 @@
return [ return [
"something_went_wrong" => "Something went wrong", "something_went_wrong" => "Something went wrong",
"error_occurred" => "An error occurred",
"retry" => "Retry", "retry" => "Retry",
"go_back" => "Go Back", "go_back" => "Go Back",
"submitting" => "Submitting", "submitting" => "Submitting",

@ -12,9 +12,6 @@
{% if site.recaptcha.enabled %} {% if site.recaptcha.enabled %}
<script src="https://www.google.com/recaptcha/api.js?render={{ site.recaptcha.key }}" nonce="{{ site.csp.nonce }}"></script> <script src="https://www.google.com/recaptcha/api.js?render={{ site.recaptcha.key }}" nonce="{{ site.csp.nonce }}"></script>
{% endif %} {% endif %}
<script nonce="{{ site.csp.nonce }}">
window.languageEntries = {{ site.language.entries|json_encode()|raw }};
</script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}

@ -7,6 +7,10 @@
{% block head %} {% block head %}
<title>{{ site.title }}</title> <title>{{ site.title }}</title>
{% endblock %} {% endblock %}
<script nonce="{{ site.csp.nonce }}">
window.languageCode = {{ site.language.code|json_encode()|raw }};
window.languageEntries = {{ site.language.entries|json_encode()|raw }};
</script>
</head> </head>
<body> <body>
{% block body %} {% block body %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -11,19 +11,24 @@ import Sidebar from "./elements/sidebar";
import LoginForm from "./views/login"; import LoginForm from "./views/login";
import {Alert} from "@material-ui/lab"; import {Alert} from "@material-ui/lab";
import {Button} from "@material-ui/core"; import {Button} from "@material-ui/core";
import {Locale} from "shared/locale"; import { LocaleContext } from "shared/locale";
const L = (key) => {
return "<nope>";
}
export default class AdminDashboard extends React.Component { export default class AdminDashboard extends React.Component {
static contextType = LocaleContext;
constructor(props) { constructor(props) {
super(props); super(props);
this.api = new API(); this.api = new API();
this.locale = Locale.getInstance();
this.state = { this.state = {
loaded: false, loaded: false,
dialog: { onClose: () => this.hideDialog() }, dialog: { onClose: () => this.hideDialog() },
info: { }, info: { },
error: null, error: null
}; };
} }
@ -40,6 +45,7 @@ export default class AdminDashboard extends React.Component {
} }
onInit() { onInit() {
// return;
this.setState({ ...this.state, loaded: false, error: null }); this.setState({ ...this.state, loaded: false, error: null });
this.api.getLanguageEntries("general").then(data => { this.api.getLanguageEntries("general").then(data => {
if (data.success) { if (data.success) {
@ -68,10 +74,6 @@ export default class AdminDashboard extends React.Component {
this.onInit(); this.onInit();
} }
onUpdateLocale() {
this.setState({ ...this.state, locale: currentLocale })
}
onLogin(username, password, rememberMe, callback) { onLogin(username, password, rememberMe, callback) {
this.setState({ ...this.state, error: "" }); this.setState({ ...this.state, error: "" });
return this.api.login(username, password, rememberMe).then((res) => { return this.api.login(username, password, rememberMe).then((res) => {
@ -126,14 +128,14 @@ export default class AdminDashboard extends React.Component {
if (!this.state.loaded) { if (!this.state.loaded) {
if (this.state.error) { if (this.state.error) {
return <Alert severity={"error"} title={"An error occurred"}> return <Alert severity={"error"} title={L("general.error_occurred")}>
<div>{this.state.error}</div> <div>{this.state.error}</div>
<Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}> <Button type={"button"} variant={"outlined"} onClick={() => this.onInit()}>
Retry Retry
</Button> </Button>
</Alert> </Alert>
} else { } else {
return <b>Loading <Icon icon={"spinner"}/></b> return <b>{L("general.loading")} <Icon icon={"spinner"}/></b>
} }
} }
@ -141,8 +143,6 @@ export default class AdminDashboard extends React.Component {
showDialog: this.showDialog.bind(this), showDialog: this.showDialog.bind(this),
api: this.api, api: this.api,
info: this.state.info, info: this.state.info,
locale: this.locale,
onUpdateLocale: this.onUpdateLocale.bind(this),
onLogout: this.onLogout.bind(this), onLogout: this.onLogout.bind(this),
onLogin: this.onLogin.bind(this), onLogin: this.onLogin.bind(this),
onTotp2FA: this.onTotp2FA.bind(this), onTotp2FA: this.onTotp2FA.bind(this),
@ -179,7 +179,7 @@ export default class AdminDashboard extends React.Component {
<Dialog {...this.state.dialog}/> <Dialog {...this.state.dialog}/>
</section> </section>
</div> </div>
<Footer /> <Footer info={this.state.info} />
</BrowserRouter> </BrowserRouter>
} }
} }

@ -1,11 +1,11 @@
import React from "react"; import React from "react";
export default function Footer() { export default function Footer(props) {
return ( return (
<footer className={"main-footer"}> <footer className={"main-footer"}>
Theme: <strong>Copyright © 2014-2019 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>&nbsp; Theme: <strong>Copyright © 2014-2019 <a href={"https://adminlte.io"}>AdminLTE.io</a>. <b>Version</b> 3.0.3</strong>&nbsp;
CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> 1.2.6 CMS: <strong><a href={"https://git.romanh.de/Projekte/web-base"}>WebBase</a></strong>. <b>Version</b> {props.info.version}
</footer> </footer>
) )
} }

@ -1,7 +1,7 @@
import React, {useState} from 'react'; import React, {useCallback, useContext, useState} from 'react';
import {L} from "shared/locale";
import {Box} from "@material-ui/core"; import {Box} from "@material-ui/core";
import {makeStyles} from "@material-ui/core/styles"; import {makeStyles} from "@material-ui/core/styles";
import {LocaleContext} from "shared/locale";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
languageFlag: { languageFlag: {
@ -15,17 +15,16 @@ export default function LanguageSelection(props) {
const api = props.api; const api = props.api;
const classes = useStyles(); const classes = useStyles();
let [languages, setLanguages] = useState(null); const [languages, setLanguages] = useState(null);
const {translate: L, setLanguageByCode} = useContext(LocaleContext);
const onSetLanguage = (code) => { const onSetLanguage = useCallback((code) => {
api.setLanguageByCode(code).then((res) => { setLanguageByCode(api, code).then((res) => {
if (res.success) { if (!res.success) {
props.onUpdateLocale();
} else {
alert(res.msg); alert(res.msg);
} }
}); });
}; }, []);
let flags = []; let flags = [];
if (languages === null) { if (languages === null) {

@ -1,6 +1,9 @@
import React from "react"; import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import AdminDashboard from "./App"; import AdminDashboard from "./App";
import {LocaleProvider} from "shared/locale";
const root = createRoot(document.getElementById('admin-panel')); const root = createRoot(document.getElementById('admin-panel'));
root.render(<AdminDashboard />); root.render(<LocaleProvider>
<AdminDashboard />
</LocaleProvider>);

@ -11,13 +11,13 @@ import {
import {makeStyles} from '@material-ui/core/styles'; import {makeStyles} from '@material-ui/core/styles';
import {Alert} from '@material-ui/lab'; import {Alert} from '@material-ui/lab';
import React, {useCallback, useEffect, useState} from "react"; import React, {useCallback, useContext, useEffect, useState} from "react";
import {Navigate} from "react-router-dom"; import {Navigate} from "react-router-dom";
import {L} from "shared/locale";
import ReplayIcon from '@material-ui/icons/Replay'; import ReplayIcon from '@material-ui/icons/Replay';
import LanguageSelection from "../elements/language-selection"; import LanguageSelection from "../elements/language-selection";
import {decodeText, encodeText, getParameter, removeParameter} from "shared/util"; import {decodeText, encodeText, getParameter, removeParameter} from "shared/util";
import Icon from "shared/elements/icon"; import Icon from "shared/elements/icon";
import {LocaleContext} from "shared/locale";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
paper: { paper: {
@ -81,24 +81,26 @@ export default function LoginForm(props) {
let [tfaState, set2FAState] = useState(0); // 0: not sent, 1: sent, 2: retry let [tfaState, set2FAState] = useState(0); // 0: not sent, 1: sent, 2: retry
let [tfaError, set2FAError] = useState(""); let [tfaError, set2FAError] = useState("");
let [error, setError] = useState(""); let [error, setError] = useState("");
let [loaded, setLoaded] = useState(0); let [loaded, setLoaded] = useState(false);
const {translate: L, currentLocale, requestModules} = useContext(LocaleContext);
const getNextUrl = () => { const getNextUrl = () => {
return getParameter("next") || "/admin"; return getParameter("next") || "/admin";
} }
const onUpdateLocale = useCallback(() => { const onUpdateLocale = useCallback(() => {
api.getLanguageEntries(["general", "account"]).then(data => { requestModules(api, ["general", "account"], currentLocale).then(data => {
setLoaded(loaded + 1); setLoaded(true);
if (!data.success) { if (!data.success) {
alert(data.msg); alert(data.msg);
} }
}); });
}, [loaded]); }, [currentLocale]);
useEffect(() => { useEffect(() => {
onUpdateLocale(); onUpdateLocale();
}, []); }, [currentLocale]);
const onLogin = useCallback(() => { const onLogin = useCallback(() => {
if (!isLoggingIn) { if (!isLoggingIn) {
@ -294,7 +296,7 @@ export default function LoginForm(props) {
</> </>
} }
if (loaded === 0) { if (!loaded) {
return <b>{L("general.loading")} <Icon icon={"spinner"}/></b> return <b>{L("general.loading")} <Icon icon={"spinner"}/></b>
} }
@ -309,7 +311,7 @@ export default function LoginForm(props) {
</div> </div>
<form className={classes.form} onSubmit={(e) => e.preventDefault()}> <form className={classes.form} onSubmit={(e) => e.preventDefault()}>
{ createForm() } { createForm() }
<LanguageSelection api={api} onUpdateLocale={onUpdateLocale} /> <LanguageSelection api={api} />
</form> </form>
</div> </div>
</Container> </Container>

@ -1,5 +1,3 @@
import {Locale} from "./locale";
export default class API { export default class API {
constructor() { constructor() {
this.loggedIn = false; this.loggedIn = false;
@ -139,68 +137,18 @@ export default class API {
} }
async setLanguage(params) { async setLanguage(params) {
let res = await this.apiCall("language/set", params); return await this.apiCall("language/set", params);
if (res.success) {
Locale.getInstance().setLocale(res.language.code);
}
return res;
} }
async setLanguageByCode(code) { async getLanguageEntries(modules, code=null, useCache=false) {
return this.setLanguage({ code: code });
}
async setLanguageByName(name) {
return this.setLanguage({ name: name });
}
async getLanguageEntries(modules, code=null, useCache=true) {
if (!Array.isArray(modules)) { if (!Array.isArray(modules)) {
modules = [modules]; modules = [modules];
} }
let locale = Locale.getInstance(); return this.apiCall("language/getEntries", {code: code, modules: modules});
if (code === null) {
code = locale.currentLocale;
if (code === null && this.loggedIn) {
code = this.user.language.code;
}
}
if (code === null) {
return { success: false, msg: "No locale selected currently" };
}
let languageEntries = {};
if (useCache) {
// remove cached modules from request array
for (const module of [...modules]) {
let moduleEntries = locale.getModule(code, module);
if (moduleEntries) {
modules.splice(modules.indexOf(module), 1);
languageEntries = {...languageEntries, [module]: moduleEntries};
}
}
}
if (modules.length > 0) {
let data = await this.apiCall("language/getEntries", { code: code, modules: modules });
if (useCache) {
if (data && data.success) {
// insert into cache
for (const [module, entries] of Object.entries(data.entries)) {
locale.loadModule(code, module, entries);
}
data.entries = {...data.entries, ...languageEntries};
data.cached = false;
}
}
return data;
} else {
return { success: true, msg: "", entries: languageEntries, code: code, cached: true };
}
} }
/*
} */
}; };

@ -1,18 +1,19 @@
// application-wide global variables // translation cache import React from 'react';
class Locale { import {createContext, useCallback, useState} from "react";
constructor() { const LocaleContext = React.createContext(null);
this.entries = {};
this.currentLocale = "en_US";
}
translate(key) { function LocaleProvider(props) {
if (this.currentLocale) { const [entries, setEntries] = useState(window.languageEntries || {});
if (this.entries.hasOwnProperty(this.currentLocale)) { const [currentLocale, setCurrentLocale] = useState(window.languageCode || "en_US");
const translate = useCallback((key) => {
if (currentLocale) {
if (entries.hasOwnProperty(currentLocale)) {
let [module, variable] = key.split("."); let [module, variable] = key.split(".");
if (module && variable && this.entries[this.currentLocale].hasOwnProperty(module)) { if (module && variable && entries[currentLocale].hasOwnProperty(module)) {
let translation = this.entries[this.currentLocale][module][variable]; let translation = entries[currentLocale][module][variable];
if (translation) { if (translation) {
return translation; return translation;
} }
@ -21,47 +22,112 @@ class Locale {
} }
return "[" + key + "]"; return "[" + key + "]";
} }, [currentLocale, entries]);
setLocale(code) { const loadModule = useCallback((code, module, newEntries) => {
this.currentLocale = code; let _entries = {...entries};
if (!this.entries.hasOwnProperty(code)) { if (!_entries.hasOwnProperty(code)) {
this.entries[code] = {}; _entries[code] = {};
} }
} if (_entries[code].hasOwnProperty(module)) {
_entries[code][module] = {..._entries[code][module], ...newEntries};
loadModule(code, module, newEntries) {
if (!this.entries.hasOwnProperty(code)) {
this.entries[code] = {};
}
if (this.entries[code].hasOwnProperty(module)) {
this.entries[code][module] = {...this.entries[code][module], ...newEntries};
} else { } else {
this.entries[code][module] = newEntries; _entries[code][module] = newEntries;
} }
} setEntries(_entries);
}, [entries]);
hasModule(code, module) { const loadModules = useCallback((code, modules) => {
return this.entries.hasOwnProperty(code) && !!this.entries[code][module]; setEntries({...entries, [code]: { ...entries[code], ...modules }});
} }, [entries]);
getModule(code, module) { const hasModule = useCallback((code, module) => {
if (this.hasModule(code, module)) { return entries.hasOwnProperty(code) && !!entries[code][module];
return this.entries[code][module]; }, [entries]);
const getModule = useCallback((code, module) => {
if (hasModule(code, module)) {
return entries[code][module];
} else { } else {
return null; return null;
} }
} }, [entries]);
static getInstance() { /** API HOOKS **/
return INSTANCE; const setLanguage = useCallback(async (api, params) => {
} let res = await api.setLanguage(params);
if (res.success) {
setCurrentLocale(res.language.code)
}
return res;
}, []);
const setLanguageByName = useCallback((api, name) => {
return setLanguage(api, {name: name});
}, [setLanguage]);
const setLanguageByCode = useCallback((api, code) => {
return setLanguage(api, {code: code});
}, [setLanguage]);
const requestModules = useCallback(async (api, modules, code=null, useCache=true) => {
if (!Array.isArray(modules)) {
modules = [modules];
}
if (code === null) {
code = currentLocale;
if (code === null && api.loggedIn) {
code = api.user.language.code;
}
}
if (code === null) {
return { success: false, msg: "No locale selected currently" };
}
let languageEntries = {};
if (useCache) {
// remove cached modules from request array
for (const module of [...modules]) {
let moduleEntries = getModule(code, module);
if (moduleEntries) {
modules.splice(modules.indexOf(module), 1);
languageEntries = {...languageEntries, [module]: moduleEntries};
}
}
}
if (modules.length > 0) {
let data = await api.apiCall("language/getEntries", { code: code, modules: modules });
if (useCache) {
if (data && data.success) {
// insert into cache
loadModules(code, data.entries);
data.entries = {...data.entries, ...languageEntries};
data.cached = false;
}
}
return data;
} else {
return { success: true, msg: "", entries: languageEntries, code: code, cached: true };
}
}, [currentLocale, getModule, loadModules]);
const ctx = {
currentLocale: currentLocale,
translate: translate,
requestModules: requestModules,
setLanguageByCode: setLanguageByCode,
};
return (
<LocaleContext.Provider value={ctx}>
{props.children}
</LocaleContext.Provider>
);
} }
let INSTANCE = new Locale(); export {LocaleContext, LocaleProvider};
function L(key) {
return Locale.getInstance().translate(key);
}
export { L, Locale };