added ability to change languages; added German

This commit is contained in:
Myzel394 2022-11-06 17:19:22 +01:00
parent 29f187320b
commit 435bbf71f2
14 changed files with 579 additions and 41 deletions

View File

@ -34,6 +34,7 @@
"notistack": "^2.0.8",
"openpgp": "^5.5.0",
"react": "^18.2.0",
"react-country-flag": "^3.0.2",
"react-dom": "^18.2.0",
"react-i18next": "^12.0.0",
"react-icons": "^4.4.0",

View File

@ -0,0 +1,384 @@
{
"general": {
"cancelLabel": "Abbrechen",
"emptyValue": "-",
"emptyUnavailableValue": "Nicht verfügbar",
"defaultValueSelection": "Standard <{{value}}>",
"defaultValueSelectionRaw": "<{{value}}>",
"booleanSelection": {
"true": "Ja",
"false": "Nein"
},
"defaultError": "Ein Fehler ist aufgetreten.",
"defaultSuccess": "Erfolgreich übernommen!",
"loading": "Lädt...",
"actionNotUndoable": "Diese Aktion kann nicht rückgängig gemacht werden!",
"copyError": "Konnte nicht in Zwischenable kopieren. Bitte kopiere den Text manuell."
},
"routes": {
"OverviewRoute": {
"title": "Überblick",
"description": "Nicht viel zu sehen hier, bisher."
},
"LoginRoute": {
"forms": {
"email": {
"title": "Anmelden",
"description": "Wir senden dir einen Verifizierungscode an deine E-Mail Adresse.",
"continueAction": "Code senden",
"form": {
"email": {
"label": "E-Mail",
"placeholder": "maxmustermann@example.com"
}
}
},
"confirmCode": {
"title": "Du hast Mail!",
"description": "Wir haben einen Code an deine E-Mail gesendet. Gib ihn hier ein, um dich anzumelden.",
"continueAction": "Anmelden",
"allowLoginFromDifferentDevices": "Anmelden von anderen Geräten erlauben",
"form": {
"code": {
"label": "Verifizierungscode",
"errors": {
"invalidChars": "Ungültiger Verifizierungscode"
}
}
}
},
"confirmFromDifferentDevice": {
"title": "Anmelden fehlgeschlagen",
"description": "Du konntest nicht angemeldet werden. Dies könnte daran legen, dass du Anmeldeungen aus anderen Geräten nicht aktiviert hast oder der Verifizierungscode inkorrekt oder abgelaufen ist."
}
}
},
"SignupRoute": {
"forms": {
"email": {
"title": "Registrieren",
"description": "Wir brauchen nur deine E-Mail und du kannst direkt loslegen!",
"continueAction": "Weiter",
"form": {
"email": {
"label": "E-Mail",
"placeholder": "maxmustermann@example.com"
}
}
},
"mailVerification": {
"title": "Du hast Mail!",
"description": "Wir haben dir eine E-Mail mit einem Link geschickt, um deinen Account zu verifizieren. Bitte überprüfe deine E-Mails und klicke auf den Link, um fortzufahren.",
"editEmail": {
"title": "E-Mail ändern?",
"description": "Möchtest du einen Schritt zurückgehen um deine E-Mail zu ändern?",
"continueAction": "Ja, E-Mail ändern"
}
}
}
},
"VerifyEmailRoute": {
"title": "Bestätige deine E-Mail",
"isLoading": "Deine E-Mail wird bestätigt...",
"isCodeInvalid": "Der Verifizierungscode ist ungültig oder abgelaufen.",
"errors": {
"code": {
"invalid": "Dieser Verifizierungscode ist ungültig"
}
}
},
"CompleteAccountRoute": {
"forms": {
"generateReports": {
"title": "E-Mail-Berichte aktivieren?",
"description": "Möchtest du vollständig verschlüsselte Berichte für deine E-Mails erstellen lassen? Nur du wirst sie entschlüsseln können. Selbst wir können sie nicht entschlüsseln.",
"continueAction": "Ja",
"cancelAction": "Nein"
},
"password": {
"title": "Passwort festlegen",
"description": "Bitte gib ein sicheres Passwort ein, damit wir deine Berichte verschlüsseln können.",
"continueAction": "Weiter",
"form": {
"password": {
"label": "Passwort",
"placeholder": "********"
},
"passwordConfirm": {
"label": "Passwort bestätigen",
"placeholder": "Gib dein Passwort erneut ein",
"mustMatchHelperText": "Passwörter stimmen nicht überein."
}
}
},
"available": {
"title": "Verschlüsselung bereits eingestellt",
"description": "Du hast die Verschlüsselung bereits eingestellt. Passwörter können momentan noch nicht geändert werden."
}
}
},
"AliasesRoute": {
"title": "Aliase",
"isInCopyMode": "Du bist im Kopier-Modus. Klicke auf einen Alias um ihn in deine Zwischenablage zu kopieren.",
"emptyState": {
"title": "Willkommen zu deinen Aliases!",
"description": "Erstelle dein erstes Alias, um loszulegen."
},
"pageActions": {
"search": {
"label": "Suche",
"placeholder": "Suche nach Namen"
}
},
"actions": {
"createRandomAlias": {
"label": "Zufälliges Alias erstellen"
},
"createCustomAlias": {
"label": "Eigenes Alias erstellen",
"description": "Du kannst dein eigenes Alias erstellen. Beachte das ein zufälliges Suffix angehangen wird, um Duplikate zu vermeiden.",
"continueAction": "Alias erstellen",
"form": {
"address": {
"label": "Adresse",
"placeholder": "awesome-fish"
}
}
}
}
},
"AliasDetailRoute": {
"title": "Alias-Details",
"sections": {
"settings": {
"title": "Einstellungen",
"description": "Diese Einstellungen gelten nur für dieses Alias. Du kannst entweder einen manuellen Wert einstellen oder auf deine Standard-Werte verweisen. Beachte das dieser Wert das Verhalten ändert. Wenn du auf einen Standard-Wert verweist, verwendet dein Alias immer den aktuellsten Wert. Wenn du also deine Standard-Werte änderst, übernimmt dein Alias diese Änderungen."
},
"notes": {
"title": "Notizen",
"form": {
"createdAt": {
"label": "Erstellungsdatum",
"empty": "Nicht verfügbar"
},
"personalNotes": {
"label": "Persönliche Notizen",
"empty": "-",
"helperText": "Hier kannst du persönliche Notizen für dieses Alias eingeben. Notizen sind verschlüsselt."
},
"websites": {
"label": "Webseiten",
"emptyText": "Du hast dieses Alias auf keiner Webseite bisher genutzt.",
"placeholder": "https://example.com",
"helperText": "Füge eine Webseite zu diesem Alias hinzu. Wird verwendet um automatisch E-Mail-Felder auszufüllen."
}
}
}
}
},
"ReportsRoute": {
"title": "Berichte",
"emptyState": {
"title": "Willkommen zu deinen Berichten!",
"description": "Hier kannst du deine E-Mail-Berichte finden. Momentan sind noch keine Berichte verfügbar. Warte, bis du eine E-Mail erhalten hast."
},
"pageActions": {
"sort": {
"List": "Berichte anhand ihrer Daten auflisten",
"GroupByAlias": "Berichte nach Alias gruppieren"
}
}
},
"ReportDetailRoute": {
"title": "Bericht-Details",
"actions": {
"delete": {
"label": "Bericht löschen",
"description": "Bist du dir sicher, dass du diesen Bericht löschen möchtest?",
"continueAction": "Bericht löschen"
}
},
"sections": {
"information": {
"title": "Email-Informationen",
"form": {
"from": {
"label": "Von"
},
"to": {
"label": "Zu"
},
"subject": {
"label": "Betreff"
}
}
},
"trackers": {
"title": "Tracker",
"results": {
"imageTrackers": {
"text_zero": "Keine Bild-Tracker gefunden",
"text_one": "Ein Bild-Tracker entfernt",
"text_other": "{{count}} Bild-Tracker entfernt"
},
"proxiedImages": {
"text_zero": "Keine Bilder gefunden",
"text_one": "Ein Bild wird weitergeleitet",
"text_other": "{{count}} Bilder werden weitergeleitet",
"status": {
"isStored": "Auf Server gespeichert",
"isProxying": "Wird weitergeleitet"
}
}
}
}
}
},
"SettingsRoute": {
"title": "Einstellungen",
"forms": {
"aliasPreferences": {
"title": "Alias-Präferenzen",
"description": "Wähle die Standard-Werte für deine Aliase aus. Dies betrifft nur Aliase, bei denen du keinen manuellen Wert gesetzt hast.",
"saveAction": "Präferenzen speichern"
}
}
},
"LogoutRoute": {
"title": "Abmelden",
"description": "Wir sind dich am abmelden..."
}
},
"components": {
"NavigationButton": {
"overview": "Überblibkc",
"aliases": "Aliase",
"reports": "Berichte",
"settings": "Einstellungen"
},
"AuthenticateRoute": {
"signup": "Registrieren",
"login": "Anmelden"
},
"AuthenticatedRoute": {
"logout": "Abmelden"
},
"EnterDecryptionPassword": {
"title": "Berichte entschlüsseln",
"description": "Bitte gib dein Passwort ein, damit deine Berichte entschlüsselt werden können.",
"cancelAction": "Später entschlüsseln",
"continueAction": "Weiter",
"form": {
"password": {
"label": "Passwort",
"placeholder": "********",
"errors": {
"invalidPassword": "Das Passwort ist ungültig"
}
}
}
},
"ResendMailButton": {
"label": "E-Mail erneut senden"
},
"OpenMailButton": {
"label": "E-Mail öffnen"
},
"DecryptionPasswordMissingAlert": {
"unavailable": {
"title": "Verschlüsselung benötigt",
"description": "Du musst die Verschlüsselung aktivieren, um dieses Feature nutzen zu können.",
"continueAction": "Verschlüsselung aktivieren"
},
"passwordRequired": {
"title": "Passwort benötigt",
"description": "Dein Passwort wird benötigt, um dieses Feature nutzen zu können.",
"continueAction": "Passwort eingeben"
}
},
"TimedButton": {
"remainingTime_one": "({{count}})",
"remainingTime_other": "({{count}})"
},
"ErrorLoadingDataMessage": {
"tryAgain": "Neu laden"
},
"AliasTypeIndicator": {
"random": "Dies ist ein zufällig-generiertes Alias",
"custom": "Dies ist ein benutzerdefiniertes Alias"
},
"NoSearchResults": {
"title": "Keine Ergebnisse gefunden",
"description": "Wir konnten keine Ergebnisse für diese Suche finden. Versuche es mit einem anderen Suchbegriff."
},
"LockNavigationContextProvider": {
"title": "Möchtest du wirklich die Seite verlassen?",
"description": "Du hast Änderungen, welche noch nicht gespeichert wurden. Wenn du jetzt diese Seite verlässt, gehen deine Änderungen verloren.",
"continueLabel": "Verlassen"
}
},
"relations": {
"alias": {
"mutations": {
"success": {
"aliasCreation": "Alias wurde erfolgreich erstellt!",
"aliasUpdated": "Alias wurde erfolgreich upgedatet!",
"notesUpdated": "Notizen wurden erfolgreich upgedated & verschlüsselt!",
"aliasChangedToEnabled": "Alias wurde aktiviert",
"aliasChangedToDisabled": "Alias wurde deaktiviert",
"addressCopiedToClipboard": "E-Mail-Adresse wurde in deine Zwischenablage kopiert!"
}
},
"settings": {
"removeTrackers": {
"label": "Tracker entfernen",
"helperText": "Entferne Einzelpixel-Tracker und URL-Tracker"
},
"createMailReports": {
"label": "E-Mail-Berichte erstellen",
"helperText": "Erstelle Berichte von E-Mails, die an Aliase gesendet werden. Berichte sind Ende-zu-Ende verschlüsselt. Nur du kannst sie entschlüsseln."
},
"proxyImages": {
"label": "Bilder weiterleiten",
"helperText": "Leitet Bilder durch diese KleckRelay-Instanz weiter. Dies stellt einen weiteren Schutz deiner Privatspähre dar. Bilder werden direkt runtergeladen nachdem wir eine E-Mail erhalten haben. Diese werden dann für eine gewisse Zeit auf dem Server gespeichert. Während dieser Zeit wird das Bild von uns an dich gesendet. Dies bedeutet, dass der Absender keine Chance haben wird, herauszufinden, dass du diese E-Mail geöffnet hast. Nach der Zeit wird das Bild vom Absender geladen, aber durch uns weitergeleitet. Dies bedeutet, dass der Absender weder auf deine IP-Adresse, noch auf deine Browserdaten zugreifen kann."
},
"imageProxyFormat": {
"label": "Bild-Format",
"enumTexts": {
"jpeg": "JPEG",
"png": "PNG",
"webp": "WEBP"
}
},
"imageProxyUserAgent": {
"label": "Bild-Weiterleitungs-User-Agent",
"helperText": "Ein User-Agent ist eine Kennzeichnung, die jeden Browser und E-Mail-Client identifiziert, wenn Dateien runtergeladen werden, so wie beispielsweise Bilder. Du kannst hier einstellen, welchen User-Agent du beim Weiterleiten verwenden möchtest. User-Agents werden aktuell gehalten.",
"enumTexts": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",
"outlook-windows": "Outlook / Windows",
"outlook-macos": "Outlook / MacOS",
"firefox": "Firefox Browser",
"chrome": "Chrome Browser"
}
},
"saveAction": "Einstellungen speichern"
}
},
"report": {
"mutations": {
"success": {
"reportDeleted": "Bericht wurde gelöscht!"
}
},
"emailMeta": {
"flow": "{{from}} -> {{to}}",
"emptySubject": "<Kein Betreff>"
}
}
}
}

View File

@ -182,7 +182,7 @@
"title": "Reports",
"emptyState": {
"title": "Welcome to your Reports!",
"description": "Here you will find your email reports. Currently, you don't have any reports. Wait until you receive an email"
"description": "Here you will find your email reports. Currently, you don't have any reports. Wait until you receive an email."
},
"pageActions": {
"sort": {
@ -225,11 +225,11 @@
},
"proxiedImages": {
"text_zero": "No images found",
"text_one": "Proxying 1 image",
"text_other": "Proxying {{count}} images",
"text_one": "Forwarding 1 image",
"text_other": "Forwarding {{count}} images",
"status": {
"isStored": "Stored on Server",
"isProxying": "Proxying image"
"isProxying": "Being forwarded"
}
}
}
@ -241,7 +241,7 @@
"forms": {
"aliasPreferences": {
"title": "Alias Preferences",
"description": "Select the default behavior for your aliases. This will only affect aliases that do not have a custom behavior set.",
"description": "Select default values for your aliases. This only affects aliases you haven't set a custom value for.",
"saveAction": "Save preferences"
}
}
@ -313,6 +313,11 @@
"NoSearchResults": {
"title": "Nothing found",
"description": "We couldn't find anything for your search query. Try again with a different query."
},
"LockNavigationContextProvider": {
"title": "Are you sure you want to leave?",
"description": "You have unsaved changes. If you leave, your changes will be lost.",
"continueLabel": "Leave"
}
},
@ -339,7 +344,7 @@
},
"proxyImages": {
"label": "Proxy Images",
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the original server has no idea you have opened the mail. After the cache time, the image is loaded from the original server, but it gets proxied by us. This means the original server will not be able to access neither your IP address nor your user agent."
"helperText": "Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the sender has no idea you have opened the mail. After the cache time, the image is loaded from the sender, but it will be forwarded by us. This means the sender will not be able to access your IP address nor your browser data."
},
"imageProxyFormat": {
"label": "Image File Type",
@ -351,7 +356,7 @@
},
"imageProxyUserAgent": {
"label": "Image Proxy User Agent",
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used by the proxy. User Agents are kept up-to-date.",
"helperText": "An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used when we forward it. User Agents are kept up-to-date.",
"enumTexts": {
"apple-mail": "Apple Mail",
"google-mail": "Google Mail",

View File

@ -17,6 +17,7 @@ import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
import LoginRoute from "~/routes/LoginRoute"
import LogoutRoute from "~/routes/LogoutRoute"
import OverviewRoute from "~/routes/OverviewRoute"
import ReportDetailRoute from "~/routes/ReportDetailRoute"
import ReportsRoute from "~/routes/ReportsRoute"
import RootRoute from "~/routes/Root"
@ -24,7 +25,6 @@ import SettingsRoute from "~/routes/SettingsRoute"
import SignupRoute from "~/routes/SignupRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
import OverviewRoute from "~/routes/OverviewRoute"
import "./init-i18n"
const router = createBrowserRouter([

View File

@ -0,0 +1,31 @@
import {ReactElement, useMemo} from "react"
import {useAsyncFn} from "react-use"
import LoadingScreen from "~/LoadingScreen"
import AppLoadingScreenContext, {AppLoadingScreenContextType} from "./AppLoadingScreenContext"
export interface AppLoadingScreenProps {
children: ReactElement
}
export default function AppLoadingScreen({children}: AppLoadingScreenProps): ReactElement {
const [state, setLoadingFunction] = useAsyncFn(callback => callback(), [])
const value = useMemo<AppLoadingScreenContextType>(
() => ({
setLoadingFunction,
}),
[setLoadingFunction],
)
if (state.loading) {
return <LoadingScreen />
}
return (
<AppLoadingScreenContext.Provider value={value}>
{children}
</AppLoadingScreenContext.Provider>
)
}

View File

@ -0,0 +1,13 @@
import {createContext} from "react"
export interface AppLoadingScreenContextType {
setLoadingFunction: (callback: () => Promise<void>) => void
}
const AppLoadingScreenContext = createContext<AppLoadingScreenContextType>({
setLoadingFunction: () => {
throw new Error("setLoadingFunction() not implemented")
},
})
export default AppLoadingScreenContext

View File

@ -6,6 +6,7 @@ export interface LockNavigationContextType {
release: () => void
navigate: (path: string) => void
handleAnchorClick: (event: React.MouseEvent<HTMLAnchorElement>) => void
showDialog: () => Promise<void>
}
const LockNavigationContext = createContext<LockNavigationContextType>({
@ -22,6 +23,9 @@ const LockNavigationContext = createContext<LockNavigationContextType>({
handleAnchorClick: () => {
throw new Error("handleAnchorClick() not implemented")
},
showDialog: () => {
throw new Error("showDialog() not implemented")
},
})
export default LockNavigationContext

View File

@ -1,8 +1,11 @@
import {TiCancel} from "react-icons/ti"
import {ReactNode, useMemo, useState} from "react"
import {ReactNode, useMemo, useRef, useState} from "react"
import {useNavigate} from "react-router-dom"
import {MdLogout} from "react-icons/md"
import {useTranslation} from "react-i18next"
import LockNavigationContext, {LockNavigationContextType} from "./LockNavigationContext"
import {
Button,
Dialog,
@ -12,10 +15,6 @@ import {
DialogTitle,
} from "@mui/material"
import LockNavigationContext, {
LockNavigationContextType,
} from "./LockNavigationContext"
export interface LockNavigationContextProviderProps {
children: ReactNode
}
@ -23,12 +22,39 @@ export interface LockNavigationContextProviderProps {
export default function LockNavigationContextProvider({
children,
}: LockNavigationContextProviderProps): JSX.Element {
const {t} = useTranslation()
const navigate = useNavigate()
const [isLocked, setIsLocked] = useState<boolean>(false)
const [nextPath, setNextPath] = useState<string | null>(null)
const [showDialog, setShowDialog] = useState<boolean>(false)
const showDialog = Boolean(nextPath)
const $continueFunction = useRef<(() => void) | null>(null)
const $cancelFunction = useRef<(() => void) | null>(null)
const cancel = () => {
setNextPath(null)
setShowDialog(false)
$cancelFunction.current?.()
$continueFunction.current = null
$cancelFunction.current = null
}
const leave = () => {
setShowDialog(false)
setIsLocked(false)
$continueFunction.current?.()
$continueFunction.current = null
$cancelFunction.current = null
if (nextPath) {
const path = new URL(nextPath as string).pathname
navigate(path)
}
setNextPath(null)
}
const value = useMemo(
(): LockNavigationContextType => ({
@ -36,8 +62,10 @@ export default function LockNavigationContextProvider({
navigate: (path: string) => {
if (isLocked) {
setNextPath(path)
setShowDialog(true)
} else {
setNextPath(null)
setShowDialog(false)
navigate(path)
}
},
@ -45,10 +73,20 @@ export default function LockNavigationContextProvider({
if (isLocked) {
event.preventDefault()
setNextPath(event.currentTarget.href)
setShowDialog(true)
}
},
lock: () => setIsLocked(true),
release: () => setIsLocked(false),
release: () => {
setIsLocked(false)
setShowDialog(false)
},
showDialog: () =>
new Promise((resolve, reject) => {
setShowDialog(true)
$continueFunction.current = resolve
$cancelFunction.current = reject
}),
}),
[isLocked],
)
@ -58,31 +96,19 @@ export default function LockNavigationContextProvider({
<LockNavigationContext.Provider value={value}>
{children}
</LockNavigationContext.Provider>
<Dialog open={showDialog} onClose={() => setNextPath(null)}>
<DialogTitle>Are you sure you want to go?</DialogTitle>
<Dialog open={showDialog} onClose={cancel}>
<DialogTitle>{t("components.LockNavigationContextProvider.title")}</DialogTitle>
<DialogContent>
<DialogContentText>
You have unsaved changes. If you leave this page, your
changes will be lost.
{t("components.LockNavigationContextProvider.description")}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
startIcon={<TiCancel />}
onClick={() => setNextPath(null)}
>
Cancel
<Button startIcon={<TiCancel />} onClick={cancel}>
{t("general.cancelLabel")}
</Button>
<Button
startIcon={<MdLogout />}
onClick={() => {
const path = new URL(nextPath as string).pathname
setNextPath(null)
setIsLocked(false)
navigate(path)
}}
>
Leave
<Button startIcon={<MdLogout />} onClick={leave}>
{t("components.LockNavigationContextProvider.continueLabel")}
</Button>
</DialogActions>
</Dialog>

View File

@ -0,0 +1,52 @@
import {ReactElement, useContext} from "react"
import {useTranslation} from "react-i18next"
import ReactCountryFlag from "react-country-flag"
import sortArray from "sort-array"
import {ListItemIcon, ListItemText, MenuItem, Select} from "@mui/material"
import {Language} from "~/server-types"
import AppLoadingScreenContext from "~/AppLoadingScreen/AppLoadingScreenContext"
import LockNavigationContext from "~/LockNavigationContext/LockNavigationContext"
const LANGUAGE_NAME_MAP = {
[Language.EN_US]: "English",
[Language.DE_DE]: "Deutsch",
}
const SORTED_ENTRIES = sortArray(Object.entries(LANGUAGE_NAME_MAP), {
by: "1",
})
export default function LanguageButton(): ReactElement {
const {setLoadingFunction} = useContext(AppLoadingScreenContext)
const {isLocked, showDialog} = useContext(LockNavigationContext)
const {i18n} = useTranslation()
return (
<Select
value={i18n.language}
fullWidth
size="small"
renderValue={value => LANGUAGE_NAME_MAP[value as Language]}
onChange={async event => {
if (isLocked) {
await showDialog()
event.preventDefault()
}
setLoadingFunction(async () => {
await i18n.changeLanguage(event.target.value as Language)
})
}}
>
{SORTED_ENTRIES.map(([language, name]) => (
<MenuItem key={language} value={language}>
<ListItemIcon>
<ReactCountryFlag countryCode={language.split("-")[1]} />
</ListItemIcon>
<ListItemText>{name}</ListItemText>
</MenuItem>
))}
</Select>
)
}

View File

@ -36,5 +36,7 @@ export * from "./SimpleInformationContainer"
export {default as SimpleInformationContainer} from "./SimpleInformationContainer"
export * from "./NoSearchResults"
export {default as NoSearchResults} from "./NoSearchResults"
export * from "./LanguageButton"
export {default as LanguageButton} from "./LanguageButton"
export * as SimplePageBuilder from "./simple-page-builder"

View File

@ -5,6 +5,8 @@ import {useTranslation} from "react-i18next"
import {Box, Button, Grid} from "@mui/material"
import {LanguageButton} from "~/components"
export default function AuthenticateRoute(): ReactElement {
const {t} = useTranslation()
@ -18,7 +20,13 @@ export default function AuthenticateRoute(): ReactElement {
>
<div />
<Outlet />
<Grid container spacing={2} justifyContent="center" marginBottom={2}>
<Grid
container
spacing={2}
justifyContent="center"
alignItems="center"
marginBottom={2}
>
<Grid item>
<Button
component={RouterLink}
@ -41,6 +49,9 @@ export default function AuthenticateRoute(): ReactElement {
{t("components.AuthenticateRoute.login")}
</Button>
</Grid>
<Grid item>
<LanguageButton />
</Grid>
</Grid>
</Box>
)

View File

@ -1,12 +1,12 @@
import {ReactElement} from "react"
import {Outlet} from "react-router-dom"
import {Link as RouterLink} from "react-router-dom"
import {Link as RouterLink, Outlet} from "react-router-dom"
import {useTranslation} from "react-i18next"
import {MdLogout} from "react-icons/md"
import {Box, Button, Grid, List, ListItem, Paper, useTheme} from "@mui/material"
import {useUser} from "~/hooks"
import {LanguageButton} from "~/components"
import LockNavigationContextProvider from "~/LockNavigationContext/LockNavigationContextProvider"
import NavigationButton, {
NavigationSection,
@ -90,6 +90,9 @@ export default function AuthenticatedRoute(): ReactElement {
{t("components.AuthenticatedRoute.logout")}
</Button>
</Grid>
<Grid item>
<LanguageButton />
</Grid>
</Grid>
</Grid>
</Grid>

View File

@ -1,6 +1,12 @@
import {Outlet} from "react-router-dom"
import React, {ReactElement} from "react"
import AppLoadingScreen from "~/AppLoadingScreen/AppLoadingScreen"
export default function RootRoute(): ReactElement {
return <Outlet />
return (
<AppLoadingScreen>
<Outlet />
</AppLoadingScreen>
)
}

View File

@ -19,7 +19,8 @@ export enum AliasType {
}
export enum Language {
EN_US = "en_US",
EN_US = "en-US",
DE_DE = "de-DE",
}
export enum Theme {
@ -159,8 +160,7 @@ export interface UserNote {
privateKey: string
}
export interface User
extends Omit<ServerUser, "encryptedNotes" | "isDecrypted"> {
export interface User extends Omit<ServerUser, "encryptedNotes" | "isDecrypted"> {
notes: UserNote
isDecrypted: true
}