diff --git a/.clabot b/.clabot index 7526e4d1..3cfa4f6a 100644 --- a/.clabot +++ b/.clabot @@ -1,4 +1,4 @@ { - "contributors": ["azukaar", "jwr1", "Jogai", "InterN0te", "catmandx", "revam", "Kawanaao", "davis4acca", "george-radu-cs"], + "contributors": ["azukaar", "jwr1", "Jogai", "InterN0te", "catmandx", "revam", "Kawanaao", "davis4acca", "george-radu-cs", "madejackson"], "message": "We require contributors to sign our [Contributor License Agreement](https://github.com/azukaar/Cosmos-Server/blob/master/cla.md). In order for us to review and merge your code, add yourself to the .clabot file as contributor, as a way of signing the CLA." } diff --git a/client/src/components/@extended/Breadcrumbs.jsx b/client/src/components/@extended/Breadcrumbs.jsx index 4bffd682..a1b97b69 100644 --- a/client/src/components/@extended/Breadcrumbs.jsx +++ b/client/src/components/@extended/Breadcrumbs.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // material-ui import MuiBreadcrumbs from '@mui/material/Breadcrumbs'; @@ -12,6 +13,7 @@ import MainCard from '../MainCard'; // ==============================|| BREADCRUMBS ||============================== // const Breadcrumbs = ({ navigation, title, ...others }) => { + const { t } = useTranslation(); const location = useLocation(); const [main, setMain] = useState(); const [item, setItem] = useState(); @@ -65,7 +67,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => { if (main && main.type === 'collapse') { mainContent = ( - {main.title} + {t(main.title)} ); } @@ -75,7 +77,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => { itemTitle = item.title; itemContent = ( - {itemTitle} + {t(itemTitle)} ); @@ -97,7 +99,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => { {title && ( - {item.title} + {t(item.title)} )} diff --git a/client/src/components/apiModal.jsx b/client/src/components/apiModal.jsx index e5d7bff0..fb8dbfba 100644 --- a/client/src/components/apiModal.jsx +++ b/client/src/components/apiModal.jsx @@ -8,6 +8,7 @@ import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import * as React from 'react'; import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; const preStyle = { backgroundColor: '#000', @@ -40,6 +41,7 @@ const preStyle = { } const ApiModal = ({ callback, label }) => { + const { t } = useTranslation(); const [openModal, setOpenModal] = useState(false); const [content, setContent] = useState(""); const [loading, setLoading] = useState(true); @@ -58,7 +60,7 @@ const ApiModal = ({ callback, label }) => { return <> setOpenModal(false)} fullWidth maxWidth={'sm'}> - Refresh Page + {t('global.refreshPage')}
@@ -71,7 +73,7 @@ const ApiModal = ({ callback, label }) => {
                   loading={loading}
               onClick={() => {   
                   getContent();         
-              }}>Refresh
+              }}>{t('global.refresh')}
               
diff --git a/client/src/components/confirmModal.jsx b/client/src/components/confirmModal.jsx
index f8a5ff0d..9721767a 100644
--- a/client/src/components/confirmModal.jsx
+++ b/client/src/components/confirmModal.jsx
@@ -8,8 +8,10 @@ import DialogContentText from '@mui/material/DialogContentText';
 import DialogTitle from '@mui/material/DialogTitle';
 import * as React from 'react';
 import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 
 const ConfirmModal = ({ callback, label, content, startIcon }) => {
+    const { t } = useTranslation();
     const [openModal, setOpenModal] = useState(false);
 
     return <>
@@ -23,12 +25,12 @@ const ConfirmModal = ({ callback, label, content, startIcon }) => {
           
               
+              }}>{t('global.cancelAction')}
                {   
                   callback();     
                   setOpenModal(false);    
-              }}>Confirm
+              }}>{t('global.confirmAction')}
           
       
@@ -55,7 +57,7 @@ const ConfirmModalDirect = ({ callback, content, onClose }) => { onClose && onClose(); setOpenModal(false); }}> - Are you sure? + {t('global.confirmDeletion')} {content} @@ -65,13 +67,13 @@ const ConfirmModalDirect = ({ callback, content, onClose }) => { + }}>{t('global.cancelAction')} { callback(); setOpenModal(false); onClose && onClose(); - }}>Confirm + }}>{t('global.confirmAction')} diff --git a/client/src/components/passwordModal.jsx b/client/src/components/passwordModal.jsx index 80f74c64..403f9eb4 100644 --- a/client/src/components/passwordModal.jsx +++ b/client/src/components/passwordModal.jsx @@ -9,15 +9,16 @@ import DialogTitle from '@mui/material/DialogTitle'; import { LoadingButton } from '@mui/lab'; import { useFormik, FormikProvider } from 'formik'; import * as Yup from 'yup'; +import { useTranslation } from 'react-i18next'; const PasswordModal = ({ textInfos, cb, OnClose }) => { - + const { t } = useTranslation(); const formik = useFormik({ initialValues: { password: '' }, validationSchema: Yup.object({ - password: Yup.string().required('Required'), + password: Yup.string().required(t('global.required')), }), onSubmit: async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); @@ -37,7 +38,7 @@ const PasswordModal = ({ textInfos, cb, OnClose }) => { <> OnClose()}> - Confirm Password + {t('auth.confirmPassword')}
@@ -46,7 +47,7 @@ const PasswordModal = ({ textInfos, cb, OnClose }) => { fullWidth id="password" name="password" - label="Your Password" + label={t('auth.yourPassword')} value={formik.values.password} type="password" onChange={formik.handleChange} @@ -64,13 +65,13 @@ const PasswordModal = ({ textInfos, cb, OnClose }) => { - + - Confirm + {t('global.confirmAction')} diff --git a/client/src/components/tableView/prettyTableView.jsx b/client/src/components/tableView/prettyTableView.jsx index ad6737b7..fe37497e 100644 --- a/client/src/components/tableView/prettyTableView.jsx +++ b/client/src/components/tableView/prettyTableView.jsx @@ -10,8 +10,10 @@ import { CircularProgress, Input, InputAdornment, Stack, TextField, useMediaQuer import { SearchOutlined } from '@ant-design/icons'; import { useTheme } from '@mui/material/styles'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const PrettyTableView = ({ isLoading, getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => { + const { t } = useTranslation(); const [search, setSearch] = React.useState(''); const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; @@ -35,7 +37,7 @@ const PrettyTableView = ({ isLoading, getKey, data, columns, sort, onRowClick, l return ( - dayjs.locale(language.toLowerCase())) + */ +// Workaround: dynamically load dayjs-locale does only with a const object, which more or less defeats de purpose --> maybe switch from dayjs to luxon or moment.js to not have to import every locale seperately +dayjsLocale(getLanguage()); // ==============================|| MAIN - REACT DOM RENDER ||============================== // @@ -37,7 +44,7 @@ root.render( - + diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx index 77a13650..129457b7 100644 --- a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; +import { Trans, useTranslation } from 'react-i18next'; // material-ui import { Box, List, Typography } from '@mui/material'; @@ -10,6 +11,7 @@ import NavItem from './NavItem'; // ==============================|| NAVIGATION - LIST GROUP ||============================== // const NavGroup = ({ item }) => { + const { t } = useTranslation(); const menu = useSelector((state) => state.menu); const { drawerOpen } = menu; @@ -39,7 +41,7 @@ const NavGroup = ({ item }) => { drawerOpen && ( - {item.title} + {t(item.title)} {/* only available in paid version */} diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx index 2e4c3986..f77369f2 100644 --- a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { forwardRef, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -14,6 +15,7 @@ import { useClientInfos } from '../../../../../utils/hooks'; // ==============================|| NAVIGATION - LIST ITEM ||============================== // const NavItem = ({ item, level }) => { + const { t } = useTranslation(); const theme = useTheme(); const dispatch = useDispatch(); const menu = useSelector((state) => state.menu); @@ -148,7 +150,7 @@ const NavItem = ({ item, level }) => { - {item.title} + {t(item.title)} } /> diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx index 347b59d6..85b6db27 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -19,7 +20,8 @@ import { Typography, useMediaQuery } from '@mui/material'; -import * as timeago from 'timeago.js'; +import { register, format } from 'timeago.js'; +import de from "timeago.js/lib/lang/de"; // project import import MainCard from '../../../../components/MainCard'; @@ -50,6 +52,8 @@ const actionSX = { // ==============================|| HEADER CONTENT - NOTIFICATION ||============================== // const Notification = () => { + register('de', de); + const { t, i18n } = useTranslation(); const theme = useTheme(); const matchesXs = useMediaQuery(theme.breakpoints.down('md')); const [notifications, setNotifications] = useState([]); @@ -199,7 +203,7 @@ const Notification = () => { > { - {notification.Title} + {t(notification.Title)}
{ paddingLeft: '8px', margin: '2px' }}> - {notification.Message} + {t(notification.Message, { Vars: notification.Vars })}
} /> - {timeago.format(notification.Date)} + {format(notification.Date, i18n.resolvedLanguage)} diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx index 245a0b07..13bdd2cd 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/Profile/index.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -54,6 +55,7 @@ function a11yProps(index) { // ==============================|| HEADER CONTENT - PROFILE ||============================== // const Profile = () => { + const { t } = useTranslation(); const theme = useTheme(); const handleLogout = async () => { @@ -173,7 +175,7 @@ const Profile = () => { textTransform: 'capitalize' }} icon={} - label="Profile" + label={t('header.profileLabel')} {...a11yProps(0)} /> { textTransform: 'capitalize' }} icon={} - label="Setting" + label={t('header.settingLabel')} {...a11yProps(1)} /> diff --git a/client/src/layout/MainLayout/Header/HeaderContent/index.jsx b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx index 2e16d0c9..4e7fc8e0 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/index.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx @@ -8,10 +8,12 @@ import Profile from './Profile'; import Notification from './Notification'; import MobileSection from './MobileSection'; import Jobs from './jobs'; +import { useTranslation } from 'react-i18next'; // ==============================|| HEADER - CONTENT ||============================== // const HeaderContent = () => { + const { t } = useTranslation(); const matchesXs = useMediaQuery((theme) => theme.breakpoints.down('md')); return ( @@ -24,7 +26,7 @@ const HeaderContent = () => { - +
{/* {!matchesXs && } diff --git a/client/src/layout/MainLayout/Header/HeaderContent/jobs.jsx b/client/src/layout/MainLayout/Header/HeaderContent/jobs.jsx index 79041c0d..109409aa 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/jobs.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/jobs.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -20,7 +21,8 @@ import { useMediaQuery, Button } from '@mui/material'; -import * as timeago from 'timeago.js'; +import { register, format } from 'timeago.js'; +import de from "timeago.js/lib/lang/de"; // project import import MainCard from '../../../../components/MainCard'; @@ -58,6 +60,8 @@ const getStatus = (job) => { } const Jobs = () => { + register('de', de); + const { t, i18n } = useTranslation(); const {role} = useClientInfos(); const isAdmin = role === "2"; const theme = useTheme(); @@ -247,8 +251,8 @@ const Jobs = () => { alignItems: 'center', }}> - {job.LastStarted == '0001-01-01T00:00:00Z' ? 'Never Run' : ( - job.Running ? {` Running - Started ${timeago.format(job.LastStarted)}`} : `Last run ${timeago.format(job.LastRun)}` + {job.LastStarted == '0001-01-01T00:00:00Z' ? t('mgmt.scheduler.list.status.neverRan') : ( + job.Running ? {` `+t('mgmt.cron.list.state.running')+` ${format(job.LastStarted, i18n.resolvedLanguage)}`} : t('mgmt.cron.list.state.lastRan')+` ${format(job.LastRun, i18n.resolvedLanguage)}` )} diff --git a/client/src/layout/MainLayout/index.jsx b/client/src/layout/MainLayout/index.jsx index b45e8e5e..9dfb2c88 100644 --- a/client/src/layout/MainLayout/index.jsx +++ b/client/src/layout/MainLayout/index.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -18,6 +19,7 @@ import { openDrawer } from '../../store/reducers/menu'; // ==============================|| MAIN LAYOUT ||============================== // const MainLayout = () => { + const { t } = useTranslation(); const theme = useTheme(); const matchDownLG = useMediaQuery(theme.breakpoints.down('xl')); const dispatch = useDispatch(); diff --git a/client/src/menu-items/dashboard.jsx b/client/src/menu-items/dashboard.jsx index a4b7a105..9406f8d6 100644 --- a/client/src/menu-items/dashboard.jsx +++ b/client/src/menu-items/dashboard.jsx @@ -10,12 +10,12 @@ const icons = { const dashboard = { id: 'group-dashboard', - title: 'Navigation', + title: 'menu-items.navigation', type: 'group', children: [ { id: 'home', - title: 'Home', + title: 'menu-items.navigation.home', type: 'item', url: '/cosmos-ui/', icon: icons.HomeOutlined, @@ -23,7 +23,7 @@ const dashboard = { }, { id: 'dashboard', - title: 'Monitoring', + title: 'menu-items.navigation.monitoringTitle', type: 'item', url: '/cosmos-ui/monitoring', icon: DashboardOutlined, @@ -32,7 +32,7 @@ const dashboard = { }, { id: 'market', - title: 'Market', + title: 'menu-items.navigation.marketTitle', type: 'item', url: '/cosmos-ui/market-listing', icon: AppstoreAddOutlined, diff --git a/client/src/menu-items/pages.jsx b/client/src/menu-items/pages.jsx index 1edcf084..6295f2db 100644 --- a/client/src/menu-items/pages.jsx +++ b/client/src/menu-items/pages.jsx @@ -13,12 +13,12 @@ const icons = { const pages = { id: 'management', - title: 'Management', + title: 'menu-items.managementTitle', type: 'group', children: [ { id: 'servapps', - title: 'ServApps', + title: 'menu-items.management.servApps', type: 'item', url: '/cosmos-ui/servapps', icon: AppstoreOutlined, @@ -26,14 +26,14 @@ const pages = { }, { id: 'url', - title: 'URLs', + title: 'menu-items.management.urls', type: 'item', url: '/cosmos-ui/config-url', icon: icons.NodeExpandOutlined, }, { id: 'storage', - title: 'Storage', + title: 'menu-items.management.storage', type: 'item', url: '/cosmos-ui/storage', icon: icons.FolderOutlined, @@ -41,7 +41,7 @@ const pages = { }, { id: 'constellation', - title: 'Constellation', + title: 'menu-items.management.constellation', type: 'item', url: '/cosmos-ui/constellation', icon: () => , @@ -49,7 +49,7 @@ const pages = { }, { id: 'users', - title: 'Users', + title: 'menu-items.management.usersTitle', type: 'item', url: '/cosmos-ui/config-users', icon: icons.ProfileOutlined, @@ -57,7 +57,7 @@ const pages = { }, { id: 'openid', - title: 'OpenID', + title: 'menu-items.management.openId', type: 'item', url: '/cosmos-ui/openid-manage', icon: PicLeftOutlined, @@ -65,7 +65,7 @@ const pages = { }, { id: 'cron', - title: 'Scheduler', + title: 'menu-items.management.schedulerTitle', type: 'item', url: '/cosmos-ui/cron', icon: ClockCircleOutlined, @@ -73,7 +73,7 @@ const pages = { }, { id: 'config', - title: 'Configuration', + title: 'menu-items.management.configurationTitle', type: 'item', url: '/cosmos-ui/config-general', icon: icons.SettingOutlined, diff --git a/client/src/menu-items/support.jsx b/client/src/menu-items/support.jsx index 9b43d5a4..12c293f9 100644 --- a/client/src/menu-items/support.jsx +++ b/client/src/menu-items/support.jsx @@ -16,12 +16,12 @@ const DiscordOutlinedIcon = (props) => { const support = { id: 'support', - title: 'Support', + title: 'menu-items.support', type: 'group', children: [ { id: 'discord', - title: 'Discord', + title: 'menu-items.support.discord', type: 'item', url: 'https://discord.com/invite/PwMWwsrwHA', icon: DiscordOutlinedIcon, @@ -30,7 +30,7 @@ const support = { }, { id: 'github', - title: 'Github', + title: 'menu-items.support.github', type: 'item', url: 'https://github.com/azukaar/Cosmos-Server', icon: GithubOutlined, @@ -39,7 +39,7 @@ const support = { }, { id: 'documentation', - title: 'Documentation', + title: 'menu-items.support.docsTitle', type: 'item', url: 'https://cosmos-cloud.io/doc', icon: QuestionOutlined, @@ -48,7 +48,7 @@ const support = { }, { id: 'bug', - title: 'Found a Bug?', + title: 'menu-items.support.bugReportTitle', type: 'item', url: 'https://github.com/azukaar/Cosmos-Server/issues/new/choose', icon: BugOutlined, diff --git a/client/src/pages/authentication/Logoff.jsx b/client/src/pages/authentication/Logoff.jsx index 3e7924fa..967f5bb5 100644 --- a/client/src/pages/authentication/Logoff.jsx +++ b/client/src/pages/authentication/Logoff.jsx @@ -10,10 +10,12 @@ import { useEffect } from 'react'; import * as API from '../../api'; import { redirectTo, redirectToLocal } from '../../utils/indexs'; +import { useTranslation } from 'react-i18next'; // ================================|| REGISTER ||================================ // const Logout = () => { + const { t } = useTranslation(); useEffect(() => { API.auth.logout() .then(() => { @@ -27,7 +29,7 @@ const Logout = () => { - You have been logged off. Redirecting you... + {t('auth.logoffText')} diff --git a/client/src/pages/authentication/auth-forms/AuthLogin.jsx b/client/src/pages/authentication/auth-forms/AuthLogin.jsx index d4ab04a2..47778934 100644 --- a/client/src/pages/authentication/auth-forms/AuthLogin.jsx +++ b/client/src/pages/authentication/auth-forms/AuthLogin.jsx @@ -17,6 +17,7 @@ import { Typography, Alert } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import * as API from "../../../api"; @@ -36,6 +37,7 @@ import { redirectToLocal } from '../../../utils/indexs'; // ============================|| FIREBASE - LOGIN ||============================ // const AuthLogin = () => { + const { t } = useTranslation(); const [checked, setChecked] = React.useState(false); const [showResetPassword, setShowResetPassword] = React.useState(false); @@ -76,17 +78,17 @@ const AuthLogin = () => { return ( <> { notLogged && - You need to be logged in to access this + {t('auth.notLoggedInError')}
} { notLoggedAdmin && - You need to be Admin + {t('auth.notAdminError')}
} { invalid && - You have been disconnected. Please login to continue + {t('auth.loggedOutError')}
} { submit: null }} validationSchema={Yup.object().shape({ - nickname: Yup.string().max(255).required('Nickname is required'), - password: Yup.string().max(255).required('Password is required') + nickname: Yup.string().max(255).required(t('global.nicknameRequiredValidation')), + password: Yup.string().max(255).required(t('auth.pwdRequired')) })} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); @@ -108,11 +110,11 @@ const AuthLogin = () => { }).catch((err) => { setStatus({ success: false }); if(err.code == 'UL001') { - setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' }); + setErrors({ submit: t('auth.wrongCredError') }); } else if (err.code == 'UL002') { - setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' }); + setErrors({ submit: t('auth.accountUnconfirmedError') }); } else { - setErrors({ submit: 'Unexpected error. Try again later.' }); + setErrors({ submit: t('auth.unexpectedErrorValidation') }); } setSubmitting(false); }); @@ -123,7 +125,7 @@ const AuthLogin = () => { - Nickname + {t('global.nicknameLabel')} { name="nickname" onBlur={handleBlur} onChange={handleChange} - placeholder="Enter your nickname" + placeholder={t('auth.usernameInput')} fullWidth error={Boolean(touched.nickname && errors.nickname)} /> @@ -144,7 +146,7 @@ const AuthLogin = () => { - Password + {t('auth.pwd')} { } - placeholder="Enter your password" + placeholder={t('auth.enterPwd')} /> {touched.password && errors.password && ( @@ -192,10 +194,10 @@ const AuthLogin = () => { label={Keep me sign in} />*/} {showResetPassword && - Forgot Your Password? + {t('auth.forgotPwd')} } {!showResetPassword && - This server does not allow password reset. + {t('auth.pwdResetNotAllowed')} } @@ -214,7 +216,7 @@ const AuthLogin = () => { variant="contained" color="primary" > - Login + {t('auth.login')} {/* diff --git a/client/src/pages/authentication/auth-forms/AuthRegister.jsx b/client/src/pages/authentication/auth-forms/AuthRegister.jsx index c0944913..dde3df0c 100644 --- a/client/src/pages/authentication/auth-forms/AuthRegister.jsx +++ b/client/src/pages/authentication/auth-forms/AuthRegister.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -38,6 +39,7 @@ import { redirectToLocal } from '../../../utils/indexs'; // ============================|| FIREBASE - REGISTER ||============================ // const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => { + const { t } = useTranslation(); const [level, setLevel] = useState(); const [showPassword, setShowPassword] = useState(false); const handleClickShowPassword = () => { @@ -50,7 +52,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => { const changePassword = (value) => { const temp = strengthIndicator(value); - setLevel(strengthColor(temp)); + setLevel(strengthColor(temp, t)); }; useEffect(() => { diff --git a/client/src/pages/authentication/forgotPassword.jsx b/client/src/pages/authentication/forgotPassword.jsx index ac9bfb62..27b45f36 100644 --- a/client/src/pages/authentication/forgotPassword.jsx +++ b/client/src/pages/authentication/forgotPassword.jsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // material-ui import { Button, FormHelperText, Grid, InputLabel, OutlinedInput, Stack, Typography } from '@mui/material'; @@ -16,6 +17,7 @@ import { useState } from 'react'; // ================================|| LOGIN ||================================ // const ForgotPassword = () => { + const { t } = useTranslation(); const [isSuccess, setIsSuccess] = useState(false); return ( @@ -35,15 +37,15 @@ const ForgotPassword = () => { email: '', }} validationSchema={Yup.object().shape({ - nickname: Yup.string().max(255).required('Nickname is required'), - email: Yup.string().email('Must be a valid email').max(255).required('Email is required'), + nickname: Yup.string().max(255).required(t('global.nicknameRequiredValidation')), + email: Yup.string().email(t('global.emailInvalidValidation')).max(255).required(t('global.emailRequiredValidation')), })} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { try { API.users.resetPassword(values).then((data) => { if (data.status == 'error') { setStatus({ success: false }); - setErrors({ submit: 'Unexpected error. Check your infos or try again later.' }); + setErrors({ submit: t('auth.unexpectedErrorValidation') }); setSubmitting(false); return; } else { @@ -65,7 +67,7 @@ const ForgotPassword = () => { @@ -91,7 +93,7 @@ const ForgotPassword = () => { variant="contained" color="primary" > - Reset Password + {t('auth.forgotPassword.resetPassword')} @@ -99,7 +101,7 @@ const ForgotPassword = () => { )} } {isSuccess &&
- Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder. + {t('auth.forgotPassword.checkEmail')}

} diff --git a/client/src/pages/authentication/newMFA.jsx b/client/src/pages/authentication/newMFA.jsx index 2e6e5129..7abba608 100644 --- a/client/src/pages/authentication/newMFA.jsx +++ b/client/src/pages/authentication/newMFA.jsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -35,6 +36,7 @@ import { CosmosCollapse } from '../config/users/formShortcuts'; import { redirectToLocal } from '../../utils/indexs'; const MFALoginForm = () => { + const { t } = useTranslation(); const urlSearchParams = new URLSearchParams(window.location.search); const redirectToURL = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui'; @@ -53,7 +55,7 @@ const MFALoginForm = () => { token: '', }} validationSchema={Yup.object().shape({ - token: Yup.string().required('Token is required').min(6, 'Token must be at least 6 characters').max(6, 'Token must be at most 6 characters'), + token: Yup.string().required(t('mgmt.openid.newMfa.tokenRequiredValidation')).min(6, t('mgmt.openid.newMfa.tokenmin6charValidation')).max(6, t('mgmt.openid.newMfa.tokenmax6charValidation')), })} onSubmit={(values, { setSubmitting, setStatus, setErrors }) => { API.users.check2FA(values.token).then((data) => { @@ -61,7 +63,7 @@ const MFALoginForm = () => { }).catch((error) => { console.log(error) setStatus({ success: false }); - setErrors({ submit: "Wrong OTP. Try again" }); + setErrors({ submit: t('mgmt.openid.newMfa.wrongOtpValidation') }); setSubmitting(false); }); }} @@ -91,7 +93,7 @@ const MFALoginForm = () => { variant="contained" loading={formik.isSubmitting} > - Login + {t('auth.login')}
@@ -129,30 +131,34 @@ const MFASetup = () => { return ( - This server requires 2FA. Scan this QR code with your authenticator app to proceed + + , ]} + /> + - ...Or enter this code manually in it + {t('mgmt.openid.newMfa.otpManualCode')} - +
{mfaCode && {mfaCode.split('?')[1].split('&').map(a =>
{decodeURI(a).replace('=', ': ')}
)}
}
- Once you have scanned the QR code or entered the code manually, enter the token from your authenticator app below + {t('mgmt.openid.newMfa.otpEnterTokenText')} - Logout + {t('global.logout')}
@@ -164,7 +170,7 @@ const NewMFA = () => ( - New MFA Setup + {t('mgmt.openid.newMfa')} @@ -179,7 +185,7 @@ const MFALogin = () => ( - Enter your OTP + {t('mgmt.openid.newMfa.enterOtp')} diff --git a/client/src/pages/config/routeConfigPage.jsx b/client/src/pages/config/routeConfigPage.jsx index 0118ccaf..391c9e03 100644 --- a/client/src/pages/config/routeConfigPage.jsx +++ b/client/src/pages/config/routeConfigPage.jsx @@ -9,8 +9,10 @@ import RouteSecurity from "./routes/routeSecurity"; import RouteOverview from "./routes/routeoverview"; import RouteMetrics from "../dashboard/routeMonitoring"; import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone"; +import { useTranslation } from 'react-i18next'; const RouteConfigPage = () => { + const { t } = useTranslation(); const { routeName } = useParams(); const [config, setConfig] = useState(null); @@ -38,18 +40,18 @@ const RouteConfigPage = () => { {config && !currentRoute &&
- Route not found + {t('mgmt.servapps.routeConfig.routeNotFound')}
} {config && currentRoute && }, { - title: 'Setup', + title: t('mgmt.servapps.routeConfig.setup'), children: r.Name)} @@ -57,18 +59,18 @@ const RouteConfigPage = () => { /> }, { - title: 'Security', + title: t('global.securityTitle'), children: }, { - title: 'Monitoring', + title: t('menu-items.navigation.monitoringTitle'), children: }, { - title: 'Events', + title: t('navigation.monitoring.eventsTitle'), children: }, ]}/>} diff --git a/client/src/pages/config/routes/newRoute.jsx b/client/src/pages/config/routes/newRoute.jsx index 8e62b3ab..0f915701 100644 --- a/client/src/pages/config/routes/newRoute.jsx +++ b/client/src/pages/config/routes/newRoute.jsx @@ -12,9 +12,11 @@ import RestartModal from '../../config/users/restart'; import RouteManagement from '../../config/routes/routeman'; import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../../utils/routes'; import HostChip from '../../../components/hostChip'; +import { useTranslation } from 'react-i18next'; const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => { + const { t } = useTranslation(); const [openRestartModal, setOpenRestartModal] = useState(false); const [submitErrors, setSubmitErrors] = useState([]); const [newRoute, setNewRoute] = useState(null); @@ -31,7 +33,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => { return <> setOpenNewModal(false)}> - New URL + {t('mgmt.urls.edit.newUrlTitle')} {openNewModal && <> @@ -77,7 +79,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => { return
{err}
})} } - + + }}>{t('global.confirmAction')} }
diff --git a/client/src/pages/config/routes/routeman.jsx b/client/src/pages/config/routes/routeman.jsx index 50cc6f6c..8e246e46 100644 --- a/client/src/pages/config/routes/routeman.jsx +++ b/client/src/pages/config/routes/routeman.jsx @@ -16,6 +16,7 @@ import { CosmosContainerPicker } from '../users/containerPicker'; import { snackit } from '../../../api/wrap'; import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes'; import { isDomain } from '../../../utils/indexs'; +import { useTranslation } from 'react-i18next'; const Hide = ({ children, h }) => { return h ?
@@ -45,6 +46,7 @@ const checkHost = debounce((host, setHostError) => { }, 500) const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => { + const { t } = useTranslation(); const [openModal, setOpenModal] = React.useState(false); const [hostError, setHostError] = React.useState(null); @@ -144,7 +146,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
{title || routeConfig.Name}
}> @@ -156,39 +158,39 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC - + - What are you trying to access with this route? + {t('mgmt.urls.edit.targetTypeInfo')} - + { (formik.values.Mode === "SERVAPP") ? @@ -202,7 +204,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC /> : @@ -210,26 +212,26 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC {formik.values.Target.startsWith('https://') && } - + - What URL do you want to access your target from? + {t('mgmt.urls.edit.sourceInfo')} {formik.values.UseHost && (<> { @@ -244,72 +246,70 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC {formik.values.UsePathPrefix && } {formik.values.UsePathPrefix && } - + - + - These settings are for advanced users only. Please do not change these unless you know what you are doing. + {t('mgmt.urls.edit.advancedSettings.advancedSettingsInfo')} - This setting will filter out all requests that do not come from the specified IPs. - This requires your setup to report the true IP of the client. By default it will, but some exotic setup (like installing docker/cosmos on Windows, or behind Cloudlfare) - will prevent Cosmos from knowing what is the client's real IP. If you used "Restrict to Constellation" above, Constellation IPs will always be allowed regardless of this setting. + {t('mgmt.urls.edit.advancedSettings.filterIpWarning')} @@ -324,7 +324,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC variant="contained" color="primary" > - Save + {t('global.saveAction')} } diff --git a/client/src/pages/config/routes/routeoverview.jsx b/client/src/pages/config/routes/routeoverview.jsx index f8fc312e..0e690a85 100644 --- a/client/src/pages/config/routes/routeoverview.jsx +++ b/client/src/pages/config/routes/routeoverview.jsx @@ -13,6 +13,7 @@ import { Field } from 'formik'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; import ImageWithPlaceholder from '../../../components/imageWithPlaceholder'; import UploadButtons from '../../../components/fileUpload'; +import { useTranslation } from 'react-i18next'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', @@ -21,6 +22,7 @@ const info = { } const RouteOverview = ({ routeConfig }) => { + const { t } = useTranslation(); const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); const [confirmDelete, setConfirmDelete] = React.useState(false); @@ -43,29 +45,29 @@ const RouteOverview = ({ routeConfig }) => { - Description + {t('global.description')}
{routeConfig.Description}
URL
- Target + {t('global.target')}
- Security + {t('global.securityTitle')}
- Monitoring + {t('menu-items.navigation.monitoringTitle')}
diff --git a/client/src/pages/config/users/configman.jsx b/client/src/pages/config/users/configman.jsx index 36b78d82..9093d369 100644 --- a/client/src/pages/config/users/configman.jsx +++ b/client/src/pages/config/users/configman.jsx @@ -36,12 +36,15 @@ import ConfirmModal from '../../../components/confirmModal'; import { DownloadFile } from '../../../api/downloadButton'; import { isDomain } from '../../../utils/indexs'; +import { Trans, useTranslation } from 'react-i18next'; + const ConfigManagement = () => { + const { t } = useTranslation(); const [config, setConfig] = React.useState(null); const [openModal, setOpenModal] = React.useState(false); const [openResartModal, setOpenRestartModal] = React.useState(false); const [uploadingBackground, setUploadingBackground] = React.useState(false); - const [saveLabel, setSaveLabel] = React.useState("Save"); + const [saveLabel, setSaveLabel] = React.useState(t('global.saveAction')); const {role} = useClientInfos(); const isAdmin = role === "2"; @@ -64,19 +67,19 @@ const ConfigManagement = () => { + }}>{t('mgmt.config.header.refreshButton.refreshLabel')} {isAdmin && } + }}>{t('mgmt.config.header.restartButton.restartLabel')}} } callback={() => { API.metrics.reset().then((res) => { refresh(); }); }} - label={'Purge Metrics Dashboard'} - content={'Are you sure you want to purge all the metrics data from the dashboards?'} /> + label={t('mgmt.config.header.purgeMetricsButton.purgeMetricsLabel')} + content={t('mgmt.config.header.purgeMetricsButton.purgeMetricsPopUp.cofirmAction')} /> {config && <> @@ -144,9 +147,9 @@ const ConfigManagement = () => { }} validationSchema={Yup.object().shape({ - Hostname: Yup.string().max(255).required('Hostname is required'), + Hostname: Yup.string().max(255).required(t('mgmt.config.http.hostnameInput.HostnameValidation')), MongoDB: Yup.string().max(512), - LoggingLevel: Yup.string().max(255).required('Logging Level is required'), + LoggingLevel: Yup.string().max(255).required(t('mgmt.config.general.logLevelInput.logLevelValidation')), })} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { @@ -223,15 +226,15 @@ const ConfigManagement = () => { return API.config.set(toSave).then((data) => { setOpenModal(true); - setSaveLabel("Saved!"); + setSaveLabel(t('global.savedConfirmation')); setTimeout(() => { - setSaveLabel("Save"); + setSaveLabel(t('global.saveAction')); }, 3000); }).catch((err) => { setOpenModal(true); - setSaveLabel("Error while saving, try again."); + setSaveLabel(t('global.savedError')); setTimeout(() => { - setSaveLabel("Save"); + setSaveLabel(t('global.saveAction')); }, 3000); }); }} @@ -261,27 +264,26 @@ const ConfigManagement = () => { } {!isAdmin &&
- As you are not an admin, you can't edit the configuration. - This page is only here for visibility. + {t('mgmt.config.general.notAdminWarning')}
} - + - This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here. + {t('mgmt.config.general.configFileInfo')} - MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional) + {t('mgmt.config.general.mongoDbInput')} { {formik.errors.MongoDB} )} - + {formik.values.PuppetModeEnabled && ( @@ -369,15 +371,15 @@ const ConfigManagement = () => { - Level of logging (Default: INFO) + {t('mgmt.config.general.logLevelInput')} { - + {!uploadingBackground && formik.values.Background && preview seems broken. Please re-upload.} {uploadingBackground && } { setUploadingBackground(true); const file = e.target.files[0]; @@ -445,7 +447,7 @@ const ConfigManagement = () => { formik.setFieldValue('Background', ""); }} > - Reset Wallpaper + {t('mgmt.config.appearance.resetWallpaperButton.resetWallpaperLabel')} @@ -471,7 +473,7 @@ const ConfigManagement = () => { - Primary Color + {t('mgmt.config.appearance.primaryColorSlider')} { - Secondary Color + {t('mgmt.config.appearance.secondaryColorSlider')} { - Hostname: This will be used to restrict access to your Cosmos Server (Your IP, or your domain name) + {t('mgmt.config.http.hostnameInput.HostnameLabel')} { {(formik.values.HTTPSCertificateMode != "DISABLED" || isDomain(formik.values.Hostname)) ? ( Allow insecure access via local IP   - - When HTTPS is used along side a domain, depending on your networking configuration, it is possible that your server is not receiving direct local connections.
- This option allows you to also access your Cosmos admin using your local IP address, like ip:port.
- You can already create ip:port URLs for your apps, but this will make them HTTP-only.}> - -
} + label={{t('mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessLabel')}   + }> + + } name="AllowHTTPLocalIPAccess" formik={formik} /> - {formik.values.AllowHTTPLocalIPAccess && - This option is not recommended as it exposes your server to security risks on your local network.
- Your local network is safer than the internet, but not safe, as devices like IoTs, smart-TVs, smartphones or even your router can be compromised.
- If you want to have a secure offline / local-only access to a server that uses a domain name and HTTPS, use Constellation instead. -
} + {formik.values.allowHTTPLocalIPAccess && }
) : ""} @@ -590,10 +585,10 @@ const ConfigManagement = () => { - Enable this option if you have a public site and want to allow search engines to find it, so it appears on search results.
+ {t('mgmt.config.http.allowSearchIndexCheckbox')}
@@ -603,13 +598,13 @@ const ConfigManagement = () => { - This allow you to setup an SMTP server for Cosmos to send emails such as password reset emails and invites. + {t('mgmt.config.email.inbobox.label')}. {formik.values.Email_Enabled && (<> @@ -628,41 +623,41 @@ const ConfigManagement = () => { /> {formik.values.Email_UseTLS && ( )} )} @@ -672,19 +667,19 @@ const ConfigManagement = () => { { - + {/* { - Geo-Blocking: (Those countries will be - {formik.values.CountryBlacklistIsWhitelist ? " allowed to access " : " blocked from accessing "} - your server) + + {t('mgmt.config.security.geoBlockSelection.geoBlockLabel', {blockAllow: formik.values.CountryBlacklistIsWhitelist ? t('mgmt.config.security.geoBlockSelection.geoBlockLabel.varAllow') : t('mgmt.config.security.geoBlockSelection.geoBlockLabel.varBlock') + })} + - + + }} variant="outlined">{t('mgmt.config.security.geoblock.resetToDefaultButton')} - + - - Use those options to restrict access to the admin panel. Be careful, if you lock yourself out, you will need to manually edit the config file. - To restrict the access to your local network, you can use the "Admin Whitelist" with the IP range 192.168.0.0/16 - + + {t('mgmt.config.security.adminRestrictions.adminRestrictionsInfo')} + - + - For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them. + {t('mgmt.config.security.encryption.enryptionInfo')} @@ -758,28 +753,28 @@ const ConfigManagement = () => { name="GenerateMissingAuthCert" as={FormControlLabel} control={} - label="Generate missing Authentication Certificates automatically (Default: true)" + label={t('mgmt.config.security.encryption.genMissingAuthCheckbox.genMissingAuthLabel')} />
{ formik.setFieldValue("ForceHTTPSCertificateRenewal", true); }} options={[ - ["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"], - ["SELFSIGNED", "Locally self-sign certificates (unsecure)"], - ["PROVIDED", "I have my own certificates"], - ["DISABLED", "Do not use HTTPS (very unsecure)"], + ["LETSENCRYPT", t('mgmt.config.security.encryption.httpsCertSelection.sslLetsEncryptChoice')], + ["SELFSIGNED", t('mgmt.config.security.encryption.httpsCertSelection.sslSelfSignedChoice')], + ["PROVIDED", t('mgmt.config.security.encryption.httpsCertSelection.sslProvidedChoice')], + ["DISABLED", t('mgmt.config.security.encryption.httpsCertSelection.sslDisabledChoice')], ]} /> { formik.setFieldValue("ForceHTTPSCertificateRenewal", true); }} @@ -793,7 +788,7 @@ const ConfigManagement = () => { onChange={(e) => { formik.setFieldValue("ForceHTTPSCertificateRenewal", true); }} - label="(optional, only if you know what you are doing) Override Wildcard Domains (comma separated, need to add both wildcard AND root domain like in the placeholder)" + label={t('mgmt.config.security.encryption.overwriteWildcardInput.overwriteWildcardLabel')} formik={formik} placeholder={"example.com,*.example.com"} /> @@ -806,7 +801,7 @@ const ConfigManagement = () => { onChange={(e) => { formik.setFieldValue("ForceHTTPSCertificateRenewal", true); }} - label="Email address for Let's Encrypt" + label={t('mgmt.config.security.encryption.sslLetsEncryptEmailInput.sslLetsEncryptEmailLabel')} formik={formik} /> ) @@ -818,7 +813,7 @@ const ConfigManagement = () => { onChange={(e) => { formik.setFieldValue("ForceHTTPSCertificateRenewal", true); }} - label="Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)" + label={t('mgmt.config.security.encryption.sslLetsEncryptDnsSelection.sslLetsEncryptDnsLabel')} name="DNSChallengeProvider" configName="DNSChallengeConfig" formik={formik} @@ -827,7 +822,7 @@ const ConfigManagement = () => { } -

Authentication Public Key

+

{t('mgmt.config.security.encryption.authPubKeyTitle')}

                         {config.HTTPConfig.AuthPublicKey}
@@ -836,7 +831,7 @@ const ConfigManagement = () => {
                   
 
                   
-                    

Root HTTPS Public Key

+

{t('mgmt.config.security.encryption.rootHttpsPubKeyTitle')}

                         {config.HTTPConfig.TLSCert}
@@ -846,7 +841,7 @@ const ConfigManagement = () => {
 
                   
                     
diff --git a/client/src/pages/config/users/containerPicker.jsx b/client/src/pages/config/users/containerPicker.jsx
index d052bddc..432b5c13 100644
--- a/client/src/pages/config/users/containerPicker.jsx
+++ b/client/src/pages/config/users/containerPicker.jsx
@@ -28,12 +28,14 @@ import AnimateButton from '../../../components/@extended/AnimateButton';
 import RestartModal from './restart';
 import Autocomplete from '@mui/material/Autocomplete';
 import CircularProgress from '@mui/material/CircularProgress';
+import { useTranslation } from 'react-i18next';
 
 import defaultport from '../../servapps/defaultport.json';
 
 import * as API  from '../../../api';
 
 export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
+  const { t } = useTranslation();
   const [open, setOpen] = React.useState(false);
   const [containers, setContainers] = React.useState([]);
   const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
@@ -177,7 +179,7 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
 
   return ( 
     
-    {label}
+    {t('mgmt.config.containerPicker.containerPortInput')}
     {!loading &&  (
         }
     {!nameOnly && <>
-      Container Port
+      {t('mgmt.config.containerPicker.containerPortInput')}
        }
       />
         {targetResult.port == '' && targetResult.port == 0 && 
-          Please select a port
+          {t('mgmt.config.containerPicker.containerPortSelection.containerPortValidation')}
         }
       
 
-      Container Protocol (use HTTP if unsure, or tcp for non-http proxying)
+      {t('mgmt.config.containerPicker.containerProtocolInput')}
       
 
-      Result Target Preview
+      {t('mgmt.config.containerPicker.targetTypePreview')}
        {
+  const { t } = useTranslation();
   const [level, setLevel] = React.useState();
   const [showPassword, setShowPassword] = React.useState(false);
   const handleClickShowPassword = () => {
@@ -88,7 +90,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
 
   const changePassword = (value) => {
       const temp = strengthIndicator(value);
-      setLevel(strengthColor(temp));
+      setLevel(strengthColor(temp, t));
   }; 
   
   React.useEffect(() => {
diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx
index 51f190ed..5934c796 100644
--- a/client/src/pages/config/users/proxyman.jsx
+++ b/client/src/pages/config/users/proxyman.jsx
@@ -40,6 +40,7 @@ import NewRouteCreate from '../routes/newRoute';
 import LazyLoad from 'react-lazyload';
 import MiniPlotComponent from '../../dashboard/components/mini-plot';
 import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
+import { useTranslation } from 'react-i18next';
 
 const stickyButton = {
   position: 'fixed',
@@ -57,6 +58,7 @@ function shorten(test) {
 }
 
 const ProxyManagement = () => {
+  const { t } = useTranslation();
   const theme = useTheme();
   const isDark = theme.palette.mode === 'dark';
   const [config, setConfig] = React.useState(null);
@@ -143,7 +145,7 @@ const ProxyManagement = () => {
 
     // if exist, increment the copy number
     do {
-      suffix += ' - Copy';
+      suffix += ' - '+t('global.copyFilenameSuffix');
     } while (routes.filter((r) => r.Name === newRoute.Name + suffix).length > 0);
 
     newRoute.Name = newRoute.Name + suffix;
@@ -164,10 +166,10 @@ const ProxyManagement = () => {
     
         
+      }}>{t('global.refresh')}  
       
+      }}>{t('global.createAction')}
     
 
     {config && <>
@@ -189,14 +191,14 @@ const ProxyManagement = () => {
             },
           },
           {
-            title: 'Enabled', 
+            title: t('global.enabled'), 
             clickable:true, 
             field: (r, k) => ,
           },
-          { title: 'URL',
+          { title: t('mgmt.config.proxy.urlTitle'),
             search: (r) => r.Name + ' ' + r.Description,
             style: {
               textDecoration: 'inherit',
@@ -207,7 +209,7 @@ const ProxyManagement = () => {
               
{r.Description}
}, - { title: 'Network', screenMin: 'lg', clickable:false, field: (r) => + { title: t('global.network'), screenMin: 'lg', clickable:false, field: (r) =>
{ ]} noLabels noBackground/>
}, - { title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => }, - { title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <> }, - { title: 'Security', screenMin: 'lg', field: (r) => , + { title: t('mgmt.config.proxy.originTitle'), screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => }, + { title: t('global.target'), screenMin: 'md', search: (r) => r.Target, field: (r) => <> }, + { title: t('global.securityTitle'), screenMin: 'lg', field: (r) => , style: {minWidth: '70px'} }, { title: '', clickable:true, field: (r, k) => { variant="contained" color="primary" > - Save Changes + {t('mgmt.config.proxy.saveChangesButton')}
@@ -285,7 +287,7 @@ const ProxyManagement = () => { } {!routes && <> - No routes configured. + {t('mgmt.config.proxy.noRoutesConfiguredText')} } diff --git a/client/src/pages/config/users/restart.jsx b/client/src/pages/config/users/restart.jsx index 40d8e173..90299775 100644 --- a/client/src/pages/config/users/restart.jsx +++ b/client/src/pages/config/users/restart.jsx @@ -22,6 +22,7 @@ import * as API from '../../../api'; import MainCard from '../../../components/MainCard'; import { useEffect, useState } from 'react'; import { isDomain } from '../../../utils/indexs'; +import { useTranslation } from 'react-i18next'; function checkIsOnline() { API.isOnline().then((res) => { @@ -34,6 +35,7 @@ function checkIsOnline() { } const RestartModal = ({openModal, setOpenModal, config, newRoute }) => { + const { t } = useTranslation(); const [isRestarting, setIsRestarting] = useState(false); const [warn, setWarn] = useState(false); const needsRefresh = config && (config.HTTPConfig.HTTPSCertificateMode == "SELFSIGNED" || @@ -45,54 +47,54 @@ const RestartModal = ({openModal, setOpenModal, config, newRoute }) => { return config ? (<> {needsRefresh && <> setOpenModal(false)}> - Refresh Page + {t('global.refreshPage')} - You need to refresh the page because you are using a self-signed certificate, in case you have to accept any new certificates. To avoid it in the future, please use Let's Encrypt. {isNotDomain && 'You are also not using a domain name, the server might go offline for a few seconds to remap your docker ports.'} + {t('mgmt.config.proxy.refreshNeededWarning.selfSigned')} {isNotDomain && t('mgmt.config.proxy.refreshNeededWarning.notDomain')} + }}>{t('global.refresh')} } {newRouteWarning && <> setOpenModal(false)}> - Certificate Renewal + {t('mgmt.config.certRenewalTitle')} - You are using Let's Encrypt but you are not using the DNS Challenge with a wildcard certificate. This means the server has to renew the certificate everytime you add a new hostname, causing a few seconds of downtime. To avoid it in the future, please refer to this link to the documentation. + {t('mgmt.config.certRenewalText')} {t('mgmt.config.certRenewalLinktext')}. + }}>{t('mgmt.config.restart.okButton')} } ) :(<> setOpenModal(false)}> - {!isRestarting ? 'Restart Server?' : 'Restarting Server...'} + {!isRestarting ? t('mgmt.config.restart.restartTitle') : t('mgmt.config.restart.restartStatus')} {warn &&
}> - The server is taking longer than expected to restart.
Consider troubleshouting the logs. If you use a self-signed certificate, you might have to refresh and re-accept it. + {t('mgmt.config.restart.restartTimeoutWarning')}
{t('mgmt.config.restart.restartTimeoutWarningTip')}
} {isRestarting ?
- : 'Do you want to restart your server?'} + : t('mgmt.config.restart.restartQuestion')}
{!isRestarting && - + + }}>{t('mgmt.servapps.actionBar.restart')} }
); diff --git a/client/src/pages/config/users/usermanagement.jsx b/client/src/pages/config/users/usermanagement.jsx index 80f48d34..3a04bceb 100644 --- a/client/src/pages/config/users/usermanagement.jsx +++ b/client/src/pages/config/users/usermanagement.jsx @@ -22,8 +22,10 @@ import * as API from '../../../api'; import MainCard from '../../../components/MainCard'; import { useEffect, useState } from 'react'; import PrettyTableView from '../../../components/tableView/prettyTableView'; +import { Trans, useTranslation } from 'react-i18next'; const UserManagement = () => { + const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [openCreateForm, setOpenCreateForm] = React.useState(false); const [openDeleteForm, setOpenDeleteForm] = React.useState(false); @@ -64,7 +66,7 @@ const UserManagement = () => { return <> {openInviteForm ? setOpenInviteForm(false)}> - Invite User + {t('mgmt.usermgmt.inviteUserTitle')}
{ }}> {toAction.emailWasSent ?
- An email has been sent with a link to {toAction.formAction}. Alternatively you can also share the link below: + {t('mgmt.usermgmt.inviteUser.emailSentConfirmation')} {t('mgmt.usermgmt.inviteUser.emailSentwithLink')} {toAction.formAction}. {t('mgmt.usermgmt.inviteUser.emailSentAltShareLink')}
:
- Send this link to {toAction.nickname} to {toAction.formAction}: + {t('mgmt.usermgmt.inviteUser.emailSentAltShare')} {toAction.nickname} {t('mgmt.usermgmt.inviteUser.emailSentAltShareTo')} {toAction.formAction}:
}
@@ -97,47 +99,47 @@ const UserManagement = () => { + }}>{t('global.close')}
: ''} setOpenDeleteForm(false)}> - Delete User + {t('mgmt.usermgmt.deleteUserTitle')} - Are you sure you want to delete user {toAction} ? + {t('mgmt.usermgmt.deleteUserConfirm')} {toAction} ? - + + }}>{t('global.delete')} setOpenEditEmail(false)}> - Edit Email + {t('mgmt.usermgmt.editEmailTitle')} - Use this form to invite edit {openEditEmail}'s Email. + {t('mgmt.usermgmt.editEmailText', { user: openEditEmail })} - + + }}>{t('global.edit')} setOpenCreateForm(false)}> - Create User + {t('mgmt.usermgmt.createUserTitle')} - Use this form to invite a new user to the system. + {t('mgmt.usermgmt.inviteUserText')} { autoFocus margin="dense" id="c-email" - label="Email Address (Optional)" + label={t('mgmt.usermgmt.createUser.emailOptInput.emailOptLabel')} type="email" fullWidth variant="standard" /> - + + }}>{t('global.createAction')}    + }}>{t('global.refresh')}  

+ }}>{t('global.createAction')}

{isLoading &&

} @@ -208,7 +210,7 @@ const UserManagement = () => { getKey={(r) => r.nickname} columns={[ { - title: 'User', + title: t('global.user'), // underline: true, field: (r) => {r.nickname}, }, @@ -221,18 +223,18 @@ const UserManagement = () => { return <>{isRegistered ? (r.role > 1 ? } - label="Admin" + label={t('mgmt.usermgmt.adminLabel')} /> : } - label="User" + label={t('global.user')} />) : ( inviteExpired ? } - label="Invite Expired" + label={t('mgmt.usermgmt.inviteExpiredLabel')} color="error" /> : } - label="Invite Pending" + label={t('mgmt.usermgmt.invitePendingLabel')} color="warning" /> )} @@ -244,16 +246,16 @@ const UserManagement = () => { field: (r) => r.email, }, { - title: 'Created At', + title: t('global.createdAt'), screenMin: 'lg', field: (r) => new Date(r.createdAt).toLocaleString(), }, { - title: 'Last Login', + title: t('mgmt.usermgmt.lastLogin'), screenMin: 'lg', field: (r) => { const hasLastLogin = new Date(r.lastLogin).getTime() > 0; - return <>{hasLastLogin ? new Date(r.lastLogin).toLocaleString() : 'Never'} + return <>{hasLastLogin ? new Date(r.lastLogin).toLocaleString() : t('global.never')} }, }, { @@ -273,13 +275,13 @@ const UserManagement = () => { setLoadingRow(r.nickname); sendlink(r.nickname, 1); } - }>Send password reset) : + }>{t('mgmt.usermgmt.sendPasswordResetButton')}) : () + } color="primary">{t('newInstall.usermgmt.inviteUser.resendInviteButton')}) }    + }>{t('global.delete')}    + }>{t('mgmt.usermgmt.reset2faButton')} } }, ]} diff --git a/client/src/pages/constellation/addDevice.jsx b/client/src/pages/constellation/addDevice.jsx index 7ee3c5f1..666cb602 100644 --- a/client/src/pages/constellation/addDevice.jsx +++ b/client/src/pages/constellation/addDevice.jsx @@ -16,6 +16,7 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from import { DownloadFile } from '../../api/downloadButton'; import QRCode from 'qrcode'; import { useClientInfos } from '../../utils/hooks'; +import { useTranslation } from 'react-i18next'; const getDocker = (data, isCompose) => { let lighthouses = ''; @@ -75,6 +76,7 @@ docker run -d \\ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => { + const { t } = useTranslation(); const [openModal, setOpenModal] = useState(false); const [isDone, setIsDone] = useState(null); const canvasRef = React.useRef(null); @@ -146,15 +148,12 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => { > {(formik) => (
- Add Device + {t('mgmt.constellation.setup.addDeviceTitle')} {isDone ?

- Device added successfully! - Download scan the QR Code from the Cosmos app or download the relevant - files to your device along side the config and network certificate to - connect: + {t('mgmt.constellation.setup.addDeviceSuccess')}

@@ -191,7 +190,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
: -

Add a Device to the constellation using either the Cosmos or Nebula client

+

{t('mgmt.constellation.setup.addDeviceText')}

{ {!formik.values.isLighthouse && (isAdmin ? { }) } /> : <> - Owner + {t('mgmt.constellation.setup.owner.label')} { @@ -243,21 +242,21 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => { {formik.values.isLighthouse && <> - + } @@ -275,7 +274,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => { - {!isDone && } + {!isDone && } @@ -294,7 +293,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => { } startIcon={} > - Add Device + {t('mgmt.constellation.setup.addDeviceTitle')} ; }; diff --git a/client/src/pages/constellation/dns.jsx b/client/src/pages/constellation/dns.jsx index 1e2bd850..b831a99e 100644 --- a/client/src/pages/constellation/dns.jsx +++ b/client/src/pages/constellation/dns.jsx @@ -14,8 +14,10 @@ import ApiModal from "../../components/apiModal"; import { isDomain } from "../../utils/indexs"; import ConfirmModal from "../../components/confirmModal"; import UploadButtons from "../../components/fileUpload"; +import { useTranslation } from 'react-i18next'; export const ConstellationDNS = () => { + const { t } = useTranslation(); const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); @@ -33,7 +35,7 @@ export const ConstellationDNS = () => { {(config) ? <>
- + { {(formik) => (
- This is a DNS that runs inside your Constellation network. It automatically - rewrites your domains DNS entries to be local to your network, and also allows you to do things like block ads - and trackers on all devices connected to your network. You can also add custom DNS entries to resolve to specific - IP addresses. This DNS server is only accessible from inside your network. + {t('mgmt.constellation.setup.dnsText')} - + - + - When changing your DNS records, always use private mode on your browser and allow some times for various caches to expire. + {t('mgmt.constellation.setup.dnsExpiryWarning')} - DNS Blocklist URLs + {t('mgmt.constellation.setup.dnsBlocklistUrls.label')} {formik.values.DNSAdditionalBlocklists && formik.values.DNSAdditionalBlocklists.map((item, index) => ( { @@ -90,7 +89,7 @@ export const ConstellationDNS = () => { + }}>{t('global.addAction')} + }}>{t('mgmt.constellation.setup.dns.resetDefault')} - + - DNS Custom Entries + {t('mgmt.constellation.setup.dns.customEntries')} {formik.values.CustomDNSEntries && formik.values.CustomDNSEntries.map((item, index) => ( { @@ -142,11 +141,11 @@ export const ConstellationDNS = () => { Value: "", Type: "A" }]); - }}>Add + }}>{t('global.addAction')} + }}>{t('mgmt.constellation.dns.resetButton')} { variant="contained" color="primary" > - Save + {t('global.saveAction')} diff --git a/client/src/pages/constellation/index.jsx b/client/src/pages/constellation/index.jsx index 13658ea3..cdd20695 100644 --- a/client/src/pages/constellation/index.jsx +++ b/client/src/pages/constellation/index.jsx @@ -8,11 +8,14 @@ import * as API from '../../api'; import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons"; import PrettyTabbedView from '../../components/tabbedView/tabbedView'; import { useClientInfos } from '../../utils/hooks'; +import { useTranslation } from 'react-i18next'; + import { ConstellationVPN } from './vpn'; import { ConstellationDNS } from './dns'; const ConstellationIndex = () => { + const { t } = useTranslation(); const {role} = useClientInfos(); const isAdmin = role === "2"; @@ -32,17 +35,15 @@ const ConstellationIndex = () => { title: 'Firewall', children:
- Coming soon. This feature will allow you to open and close ports individually - on each device and decide who can access them. + {t('mgmt.constellation.setup.firewallInfo')}
, }, { - title: 'Unsafe Routes', + title: t('mgmt.constellation.setup.unsafeRoutesTitle'), children:
- Coming soon. This feature will allow you to tunnel your traffic through - your devices to things outside of your constellation. + {t('mgmt.constellation.setup.unsafeRoutesText')}
, } diff --git a/client/src/pages/constellation/vpn.jsx b/client/src/pages/constellation/vpn.jsx index aedd29d7..c9456f15 100644 --- a/client/src/pages/constellation/vpn.jsx +++ b/client/src/pages/constellation/vpn.jsx @@ -15,6 +15,7 @@ import { isDomain } from "../../utils/indexs"; import ConfirmModal from "../../components/confirmModal"; import UploadButtons from "../../components/fileUpload"; import { useClientInfos } from "../../utils/hooks"; +import { Trans, useTranslation } from 'react-i18next'; const getDefaultConstellationHostname = (config) => { // if domain is set, use it @@ -26,6 +27,7 @@ const getDefaultConstellationHostname = (config) => { } export const ConstellationVPN = () => { + const { t } = useTranslation(); const [config, setConfig] = useState(null); const [users, setUsers] = useState(null); const [devices, setDevices] = useState(null); @@ -68,17 +70,15 @@ export const ConstellationVPN = () => {
- Constellation is a VPN that runs inside your Cosmos network. It automatically - connects all your devices together, and allows you to access them from anywhere. - Please refer to the documentation for more information. - In order to connect, please use the Constellation App. - Constellation is currently free to use until the end of the beta, planned January 2024. + , ]} + /> - + {config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <> - You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices. + {t('mgmt.constellation.externalText')} } { await API.constellation.restart(); }} > - Restart VPN Service + {t('mgmt.constellation.restartButton')} - - + + { await API.constellation.reset(); refreshConfig(); }} /> } - + {config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <> {formik.values.Enabled && <> - - + + {!formik.values.PrivateNode && <> - This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. If you change this value, you will need to reset your network and reconnect all the clients! - + + } } } @@ -146,11 +146,11 @@ export const ConstellationVPN = () => { variant="contained" color="primary" > - Save + {t('global.saveAction')} { @@ -182,19 +182,19 @@ export const ConstellationVPN = () => { field: getIcon, }, { - title: 'Device Name', + title: t('mgmt.constellation.setup.deviceName.label'), field: (r) => {r.deviceName}, }, { - title: 'Owner', + title: t('mgmt.constellation.setup.owner.label'), field: (r) => {r.nickname}, }, { - title: 'Type', + title: t('mgmt.storage.typeTitle'), field: (r) => {r.isLighthouse ? "Lighthouse" : "Client"}, }, { - title: 'Constellation IP', + title: t('mgmt.constellation.setup.ipTitle'), screenMin: 'md', field: (r) => r.ip, }, diff --git a/client/src/pages/cron/jobLogs.jsx b/client/src/pages/cron/jobLogs.jsx index 3e2ca83d..1fa7bed9 100644 --- a/client/src/pages/cron/jobLogs.jsx +++ b/client/src/pages/cron/jobLogs.jsx @@ -16,6 +16,7 @@ import UploadButtons from "../../components/fileUpload"; import { useTheme } from '@mui/material/styles'; import MiniPlotComponent from '../dashboard/components/mini-plot'; import LogLine from "../../components/logLine"; +import { useTranslation } from 'react-i18next'; const preStyle = { backgroundColor: '#000', @@ -48,6 +49,7 @@ const preStyle = { } const JobLogsDialog = ({job, OnClose}) => { + const { t } = useTranslation(); const [jobFull, setJobFull] = useState(null); useEffect(() => { @@ -61,7 +63,7 @@ const JobLogsDialog = ({job, OnClose}) => { return { OnClose && OnClose(); }}> - Last logs for {job.Name} + {t('mgmt.scheduler.lastLogs')} {job.Name}
@@ -74,7 +76,7 @@ const JobLogsDialog = ({job, OnClose}) => {
       
           
+          }}>{t('global.close')}
       
   
}; diff --git a/client/src/pages/cron/jobsManage.jsx b/client/src/pages/cron/jobsManage.jsx index 364283e3..11640310 100644 --- a/client/src/pages/cron/jobsManage.jsx +++ b/client/src/pages/cron/jobsManage.jsx @@ -17,6 +17,7 @@ import ResponsiveButton from "../../components/responseiveButton"; import MenuButton from "../../components/MenuButton"; import JobLogsDialog from "./jobLogs"; import NewJobDialog from "./newJob"; +import { useTranslation } from 'react-i18next'; const getStatus = (job) => { if (job.Running) return 'running'; @@ -26,6 +27,7 @@ const getStatus = (job) => { } export const CronManager = () => { + const { t } = useTranslation(); const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); const [cronJobs, setCronJobs] = useState([]); @@ -90,16 +92,16 @@ export const CronManager = () => { } onClick={() => { setNewJob(true); - }}>New Job + }}>{t('mgmt.cron.newCronTitle')} } onClick={() => { refresh(); - }}>Refresh + }}>{t('global.refresh')} {Object.keys(cronJobs).map(scheduler =>

{({ - "Custom": "Custom Jobs", - "SnapRAID": "Parity Disks Jobs", - "__OT__SnapRAID": "One Time Jobs", + "Custom": t('mgmt.scheduler.customJobsTitle'), + "SnapRAID": t('mgmt.scheduler.parityDiskJobsTitle'), + "__OT__SnapRAID": t('mgmt.scheduler.oneTimeJobsTitle'), }[scheduler])}

{ ]} columns={[ (scheduler == "Custom" && { - title: 'Enabled', + title: t('global.enabled'), clickable:true, field: (r, k) => setEnabled(r.Name, r.Disabled)} @@ -117,21 +119,21 @@ export const CronManager = () => { />, }), { - title: 'Name', + title: t('global.nameTitle'), field: (r) => r.Name, }, { - title: 'Schedule', - field: (r) => crontabToText(r.Crontab), + title: t('mgmt.scheduler.list.scheduleTitle'), + field: (r) => crontabToText(r.Crontab, t), }, { - title: "Status", + title: t('global.statusTitle'), field: (r) => { return
{{ - 'running': } severity={'info'} color={'info'}>Running since {r.LastStarted}, - 'success': Last run finished on {r.LastRun}, duration {(new Date(r.LastRun).getTime() - new Date(r.LastStarted).getTime()) / 1000}s, - 'error': Last run exited with an error on {r.LastRun}, - 'never': Never ran + 'running': } severity={'info'} color={'info'}>{t('mgmt.scheduler.list.status.runningSince')}{r.LastStarted}, + 'success': {t('mgmt.scheduler.list.status.lastRunFinishedOn')} {r.LastRun}, {t('mgmt.scheduler.list.status.lastRunFinishedOn.duration')} {(new Date(r.LastRun).getTime() - new Date(r.LastStarted).getTime()) / 1000}s, + 'error': {t('mgmt.scheduler.list.status.lastRunExitedOn')} {r.LastRun}, + 'never': {t('mgmt.scheduler.list.status.neverRan')} }[getStatus(r)]}
} @@ -144,19 +146,19 @@ export const CronManager = () => { refresh(); }); }}> - : { + : { API.cron.run(scheduler, r.Name).then(() => { refresh(); }); }}>} - { + { setJobLogs(r); }}> {scheduler == "Custom" && <> - { + { setNewJob(r); }}> - + { deleteCronJob(r); }} /> diff --git a/client/src/pages/cron/newJob.jsx b/client/src/pages/cron/newJob.jsx index 5cc5ac66..448ad595 100644 --- a/client/src/pages/cron/newJob.jsx +++ b/client/src/pages/cron/newJob.jsx @@ -18,8 +18,10 @@ import MiniPlotComponent from '../dashboard/components/mini-plot'; import LogLine from "../../components/logLine"; import { CosmosContainerPicker } from '../config/users/containerPicker'; import * as yup from "yup"; +import { useTranslation } from 'react-i18next'; const NewJobDialog = ({job, OnClose, refresh}) => { + const { t } = useTranslation(); const isEdit = job && typeof job === 'object'; const [config, setConfig] = useState(null); @@ -80,18 +82,18 @@ const NewJobDialog = ({job, OnClose, refresh}) => { }}>
- {isEdit ? 'Edit': 'Add'} job + {t('mgmt.cron.editCronTitle')}
- Create a custom job to run a shell command in a container. Leave the container field empty to run on the host (Running on the host only works if Cosmos is not itself running in a container). + {t('mgmt.cron.editCron.customText')} ({t('mgmt.cron.editCron.customText.onHostOnly')}).
{ fullWidth id="Crontab" name="Crontab" - label="Schedule (using crontab syntax)" + label={t('mgmt.cron.newCron.crontabInput.crontabLabel')} value={formik.values.Crontab} onChange={formik.handleChange} error={formik.touched.Crontab && Boolean(formik.errors.Crontab)} helperText={formik.touched.Crontab && formik.errors.Crontab} /> - {crontabToText(formik.values.Crontab)} + {crontabToText(formik.values.Crontab, t)} { onTargetChange={(_, name) => { formik.setFieldValue('Container', name); }} - name="Container" + name='Container' nameOnly /> @@ -137,10 +139,10 @@ const NewJobDialog = ({job, OnClose, refresh}) => { + }}>{t('global.close')} { formik.handleSubmit(); - }}>Submit + }}>{t('mgmt.cron.newCron.submitButton')}
diff --git a/client/src/pages/dashboard/AlertPage.jsx b/client/src/pages/dashboard/AlertPage.jsx index abe0cff9..f2068a48 100644 --- a/client/src/pages/dashboard/AlertPage.jsx +++ b/client/src/pages/dashboard/AlertPage.jsx @@ -29,6 +29,7 @@ import PrettyTableView from '../../components/tableView/prettyTableView'; import { DeleteButton } from '../../components/delete'; import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts'; import { MetricPicker } from './MetricsPicker'; +import { useTranslation } from 'react-i18next'; const DisplayOperator = (operator) => { switch (operator) { @@ -42,18 +43,20 @@ const DisplayOperator = (operator) => { return '?'; } } + const AlertValidationSchema = Yup.object().shape({ - name: Yup.string().required('Name is required'), - trackingMetric: Yup.string().required('Tracking metric is required'), - conditionOperator: Yup.string().required('Condition operator is required'), - conditionValue: Yup.number().required('Condition value is required'), - period: Yup.string().required('Period is required'), + name: Yup.string().required('Name is Required'), + trackingMetric: Yup.string().required('Tracking Metric is Required'), + conditionOperator: Yup.string().required('Condition Operator is Required'), + conditionValue: Yup.number().required('Condition Value is Required'), + period: Yup.string().required('Period is Required'), }); const EditAlertModal = ({ open, onClose, onSave }) => { + const { t } = useTranslation(); const formik = useFormik({ initialValues: { - name: open.Name || 'New Alert', + name: open.Name || t('navigation.monitoring.alerts.newAlertButton'), trackingMetric: open.TrackingMetric || '', conditionOperator: (open.Condition && open.Condition.Operator) || 'gt', conditionValue: (open.Condition && open.Condition.Value) || 0, @@ -73,27 +76,27 @@ const EditAlertModal = ({ open, onClose, onSave }) => { return ( - Edit Alert + {t('navigation.monitoring.alerts.action.edit')}
'], @@ -104,31 +107,31 @@ const EditAlertModal = ({ open, onClose, onSave }) => { { - + {formik.values.actions .map((action, index) => { return !action.removed && <> {action.Type === 'stop' && - Stop action will attempt to stop/disable any resources (ex. Containers, routes, etc... ) attachted to the metric. - This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU + {t('navigation.monitoring.alerts.actions.stopActionInfo')} } {action.Type === 'restart' && - Restart action will attempt to restart any Containers attachted to the metric. - This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU + {t('navigation.monitoring.alerts.actions.restartActionInfo')} } { }}> @@ -200,15 +201,15 @@ const EditAlertModal = ({ open, onClose, onSave }) => { }, ]); }}> - Add Action + {t('mgmt.monitoring.alerts.addActionButton')} - - + +
@@ -217,6 +218,7 @@ const EditAlertModal = ({ open, onClose, onSave }) => { }; const AlertPage = () => { + const { t } = useTranslation(); const [config, setConfig] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [openModal, setOpenModal] = React.useState(false); @@ -318,15 +320,15 @@ const AlertPage = () => { }, "Actions": [ { - "Type": "notification", + "mgmt.storage.typeTitle": "notification", "Target": "" }, { - "Type": "email", + "mgmt.storage.typeTitle": "email", "Target": "" }, { - "Type": "stop", + "mgmt.storage.typeTitle": "stop", "Target": "" } ], @@ -346,15 +348,15 @@ const AlertPage = () => { }, "Actions": [ { - "Type": "notification", + "mgmt.storage.typeTitle": "notification", "Target": "" }, { - "Type": "email", + "mgmt.storage.typeTitle": "email", "Target": "" }, { - "Type": "stop", + "mgmt.storage.typeTitle": "stop", "Target": "" } ], @@ -362,8 +364,28 @@ const AlertPage = () => { "Throttled": false, "Severity": "warn" }, + "Disk Health": { + "Name": 'DiskHealth', + "Enabled": true, + "Period": "latest", + "TrackingMetric": "system.disk-health.temperature.*", + "Condition": { + "Percent": false, + "Operator": "gt", + "Value": 50 + }, + "Actions": [ + { + "mgmt.storage.typeTitle": "notification", + "Target": "" + } + ], + "LastTriggered": "0001-01-01T00:00:00Z", + "Throttled": true, + "Severity": "warn" + }, "Disk Full Notification": { - "Name": "Disk Full Notification", + "Name": 'Disk Full Notification', "Enabled": true, "Period": "latest", "TrackingMetric": "cosmos.system.disk./", @@ -374,7 +396,7 @@ const AlertPage = () => { }, "Actions": [ { - "Type": "notification", + "mgmt.storage.typeTitle": "notification", "Target": "" } ], @@ -409,13 +431,13 @@ const AlertPage = () => { + }}>{t('global.refresh')} + }}>{t('global.createAction')} + }}>{t('navigation.monitoring.alerts.resetToDefaultButton')} {config && <> @@ -452,7 +474,7 @@ const AlertPage = () => { columns={[ { - title: 'Enabled', + title: t('global.enabled'), clickable:true, field: (r, k) => { }, }, { - title: 'Name', + title: t('global.nameTitle'), field: (r) => <> {r.Name}, }, { - title: 'Tracking Metric', + title: t('navigation.monitoring.alerts.trackingMetricTitle'), field: (r) => metrics[r.TrackingMetric] ? metrics[r.TrackingMetric] : r.TrackingMetric, }, { - title: 'Condition', + title: t('navigation.monitoring.alerts.conditionTitle'), screenMin: 'md', field: (r) => DisplayOperator(r.Condition.Operator) + ' ' + r.Condition.Value + (r.Condition.Percent ? '%' : ''), }, { - title: 'Period', - field: (r) => r.Period, + title: t('navigation.monitoring.alerts.periodTitle'), + field: (r) => t(r.Period), }, { - title: 'Last Triggered', + title: t('navigation.monitoring.alerts.astTriggeredTitle'), screenMin: 'md', - field: (r) => (r.LastTriggered != "0001-01-01T00:00:00Z") ? new Date(r.LastTriggered).toLocaleString() : 'Never', + field: (r) => (r.LastTriggered != "0001-01-01T00:00:00Z") ? new Date(r.LastTriggered).toLocaleString() : t('global.never'), }, { - title: 'Actions', + title: t('navigation.monitoring.alerts.actionsTitle'), field: (r) => r.Actions.map((a) => a.Type).join(', '), screenMin: 'md', }, diff --git a/client/src/pages/dashboard/MetricHeaders.jsx b/client/src/pages/dashboard/MetricHeaders.jsx index a98f8b24..a2a9b70a 100644 --- a/client/src/pages/dashboard/MetricHeaders.jsx +++ b/client/src/pages/dashboard/MetricHeaders.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -9,6 +10,7 @@ import { import { formatDate } from './components/utils'; const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => { + const { t } = useTranslation(); const resetZoom = () => { setZoom({ xaxis: {} @@ -48,7 +50,7 @@ const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => { color={slot === 'latest' ? 'primary' : 'secondary'} variant={slot === 'latest' ? 'outlined' : 'text'} > - Latest + {t('navigation.monitoring.latest')} {zoom.xaxis.min && }
} diff --git a/client/src/pages/dashboard/containerMetrics.jsx b/client/src/pages/dashboard/containerMetrics.jsx index 1f81065a..70cf0460 100644 --- a/client/src/pages/dashboard/containerMetrics.jsx +++ b/client/src/pages/dashboard/containerMetrics.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -15,6 +16,7 @@ import PlotComponent from './components/plot'; import { formatDate } from './components/utils'; const ContainerMetrics = ({containerName}) => { + const { t } = useTranslation(); const [slot, setSlot] = useState('latest'); const [zoom, setZoom] = useState({ @@ -115,7 +117,7 @@ const ContainerMetrics = ({containerName}) => { color={slot === 'latest' ? 'primary' : 'secondary'} variant={slot === 'latest' ? 'outlined' : 'text'} > - Latest + {t('navigation.monitoring.latest')} {zoom.xaxis.min && } - + - +
} diff --git a/client/src/pages/dashboard/eventsExplorer.jsx b/client/src/pages/dashboard/eventsExplorer.jsx index e94a3378..c5571eae 100644 --- a/client/src/pages/dashboard/eventsExplorer.jsx +++ b/client/src/pages/dashboard/eventsExplorer.jsx @@ -4,12 +4,16 @@ import * as API from '../../api'; import { Button, CircularProgress, Stack, TextField } from "@mui/material"; import { CosmosCollapse, CosmosSelect } from "../config/users/formShortcuts"; import MainCard from '../../components/MainCard'; -import * as timeago from 'timeago.js'; +import { register, format } from 'timeago.js'; +import de from "timeago.js/lib/lang/de"; import { ExclamationOutlined, SettingOutlined } from "@ant-design/icons"; import { Alert } from "@mui/material"; import { DownloadFile } from "../../api/downloadButton"; +import { Trans, useTranslation } from 'react-i18next'; const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = ''}) => { + register('de', de); + const { t, i18n } = useTranslation(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(initSearch); @@ -118,7 +122,7 @@ const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = ''
+ }} style={{height: '42px'}}>{t('global.refresh')}
- setSearch(e.target.value)} placeholder='Search (text or bson)' /> + setSearch(e.target.value)} placeholder={t('navigation.monitoring.events.searchInput.searchPlaceholder')} />
-
- {total} events found from {from.toLocaleString()} to {to.toLocaleString()} +
{events && @@ -168,7 +171,7 @@ const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = '' event.level == "debug" ? : event.level == "important" ? : undefined }>
{event.label}
-
{(new Date(event.date)).toLocaleString()} - {timeago.format(event.date)}
+
{(new Date(event.date)).toLocaleString()} - {format(event.date, i18n.resolvedLanguage)}
{event.eventId} - {event.object}
}>
@@ -191,7 +194,7 @@ const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = '' + }}>{t('navigation.monitoring.events.loadMoreButton')} } }
diff --git a/client/src/pages/dashboard/eventsExplorerStandalone.jsx b/client/src/pages/dashboard/eventsExplorerStandalone.jsx index dca6af29..5bd1e834 100644 --- a/client/src/pages/dashboard/eventsExplorerStandalone.jsx +++ b/client/src/pages/dashboard/eventsExplorerStandalone.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting -import 'dayjs/locale/en-gb'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -16,9 +16,9 @@ import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import dayjs from 'dayjs'; dayjs.extend(localizedFormat); // if needed -dayjs.locale('en-gb'); const EventExplorerStandalone = ({initSearch, initLevel}) => { + const { t } = useTranslation(); // one hour ago const now = dayjs(); const [from, setFrom] = useState(now.subtract(1, 'hour')); @@ -29,11 +29,11 @@ const EventExplorerStandalone = ({initSearch, initLevel}) => {
- Events + {t('navigation.monitoring.eventsTitle')} - setFrom(e)} /> - setTo(e)} /> + setFrom(e)} /> + setTo(e)} /> diff --git a/client/src/pages/dashboard/index.jsx b/client/src/pages/dashboard/index.jsx index 36e6b06d..afc19095 100644 --- a/client/src/pages/dashboard/index.jsx +++ b/client/src/pages/dashboard/index.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; // material-ui import { @@ -23,6 +24,7 @@ import EventsExplorer from './eventsExplorer'; import MetricHeaders from './MetricHeaders'; const DashboardDefault = () => { + const { t } = useTranslation(); const [value, setValue] = useState('today'); const [slot, setSlot] = useState('latest'); const [currentTab, setCurrentTab] = useState(0); @@ -132,7 +134,7 @@ const DashboardDefault = () => { {metrics &&
- Server Monitoring + {t('navigation.monitoringTitle')} {currentTab <= 2 && } {currentTab > 2 &&
}
@@ -149,19 +151,19 @@ const DashboardDefault = () => { isLoading={!metrics} tabs={[ { - title: 'Resources', + title: t('navigation.monitoring.resourcesTitle'), children: }, { - title: 'Proxy', + title: t('navigation.monitoring.proxyTitle'), children: }, { - title: 'Events', + title: t('navigation.monitoring.eventsTitle'), children: }, { - title: 'Alerts', + title: t('navigation.monitoring.alertsTitle'), children: }, ]} diff --git a/client/src/pages/dashboard/proxyDashboard.jsx b/client/src/pages/dashboard/proxyDashboard.jsx index e6811b9f..3d8cd914 100644 --- a/client/src/pages/dashboard/proxyDashboard.jsx +++ b/client/src/pages/dashboard/proxyDashboard.jsx @@ -3,35 +3,37 @@ import { LinearProgress, Tooltip, } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import PlotComponent from './components/plot'; import TableComponent from './components/table'; import { InfoCircleOutlined } from '@ant-design/icons'; const ProxyDashboard = ({ xAxis, zoom, setZoom, slot, metrics }) => { + const { t } = useTranslation(); return (<> - - - key.startsWith("cosmos.proxy.route.")).map((key) => metrics[key]) } /> - @@ -39,13 +41,13 @@ const ProxyDashboard = ({ xAxis, zoom, setZoom, slot, metrics }) => { - Reasons For Blocked Requests -
bots: Bots
-
geo: By Geolocation (blocked countries)
-
referer: By Referer
-
hostname: By Hostname (usually IP scanning threat)
-
ip-whitelists: By IP Whitelists (Including restricted to Constellation)
-
smart-shield: Smart Shield (various abuse metrics such as time, size, brute-force, concurrent requests, etc...). It does not include blocking for banned IP to save resources in case of potential attacks
+ {t('navigation.monitoring.resourceDashboard.blockReasonTitle')} +
bots: {t('navigation.monitoring.resourceDashboard.reasonByBots')}
+
geo: {t('navigation.monitoring.resourceDashboard.reasonByGeo')}
+
referer: {t('navigation.monitoring.resourceDashboard.reasonByRef')}
+
hostname: {t('navigation.monitoring.resourceDashboard.reasonByHostname')}
+
ip-whitelists: {t('navigation.monitoring.resourceDashboard.reasonByWhitelist')}
+
smart-shield: {t('navigation.monitoring.resourceDashboard.reasonBySmartShield')}
}> } data={ Object.keys(metrics).filter((key) => key.startsWith("cosmos.proxy.blocked.")).map((key) => metrics[key]) diff --git a/client/src/pages/dashboard/resourceDashboard.jsx b/client/src/pages/dashboard/resourceDashboard.jsx index 8754f496..e56e0978 100644 --- a/client/src/pages/dashboard/resourceDashboard.jsx +++ b/client/src/pages/dashboard/resourceDashboard.jsx @@ -2,31 +2,33 @@ import { Grid, LinearProgress, } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import PlotComponent from './components/plot'; import TableComponent from './components/table'; const ResourceDashboard = ({ xAxis, zoom, setZoom, slot, metrics }) => { + const { t } = useTranslation(); return (<> - + - key.startsWith("cosmos.system.docker.cpu") || key.startsWith("cosmos.system.docker.ram")).map((key) => metrics[key]) } /> - + - key.startsWith("cosmos.system.docker.net")).map((key) => metrics[key]) } /> - { let percent = value / metric.Max * 100; return @@ -46,7 +48,7 @@ const ResourceDashboard = ({ xAxis, zoom, setZoom, slot, metrics }) => { zoom={zoom} setZoom={setZoom} xAxis={xAxis} slot={slot} - title={'Temperature'} + title={t('global.temperature')} withSelector={'cosmos.system.temp.all'} SimpleDesign data={Object.keys(metrics).filter((key) => key.startsWith("cosmos.system.temp")).map((key) => metrics[key])} diff --git a/client/src/pages/dashboard/routeMonitoring.jsx b/client/src/pages/dashboard/routeMonitoring.jsx index d6ba63cc..3134cf06 100644 --- a/client/src/pages/dashboard/routeMonitoring.jsx +++ b/client/src/pages/dashboard/routeMonitoring.jsx @@ -27,6 +27,7 @@ import PlotComponent from './components/plot'; import TableComponent from './components/table'; import { HomeBackground, TransparentHeader } from '../home'; import { formatDate } from './components/utils'; +import { useTranslation } from 'react-i18next'; // avatar style const avatarSX = { @@ -64,6 +65,7 @@ const status = [ // ==============================|| DASHBOARD - DEFAULT ||============================== // const RouteMetrics = ({routeName}) => { + const { t } = useTranslation(); const [value, setValue] = useState('today'); const [slot, setSlot] = useState('latest'); @@ -166,7 +168,7 @@ const RouteMetrics = ({routeName}) => { color={slot === 'latest' ? 'primary' : 'secondary'} variant={slot === 'latest' ? 'outlined' : 'text'} > - Latest + {t('navigation.monitoring.latest')} {zoom.xaxis.min && } diff --git a/client/src/pages/home/index.jsx b/client/src/pages/home/index.jsx index 5235f416..b91d4d33 100644 --- a/client/src/pages/home/index.jsx +++ b/client/src/pages/home/index.jsx @@ -15,6 +15,7 @@ import { useClientInfos } from "../../utils/hooks"; import { FormaterForMetric, formatDate } from "../dashboard/components/utils"; import MiniPlotComponent from "../dashboard/components/mini-plot"; import Migrate014 from "./migrate014"; +import { Trans, useTranslation } from 'react-i18next'; export const HomeBackground = () => { @@ -83,6 +84,7 @@ export const TransparentHeader = () => { } const HomePage = () => { + const { t } = useTranslation(); const { routeName } = useParams(); const [servApps, setServApps] = useState([]); const [config, setConfig] = useState(null); @@ -185,7 +187,6 @@ const HomePage = () => { const primCol = theme.palette.primary.main.replace('rgb(', 'rgba(') const secCol = theme.palette.secondary.main.replace('rgb(', 'rgba(') - const optionsRadial = { plotOptions: { radialBar: { @@ -283,13 +284,13 @@ const HomePage = () => { {isAdmin && coStatus && !coStatus.database && ( - Database cannot connect, this will impact multiple feature of Cosmos. Please fix ASAP! + {t('navigation.home.dbCantConnectError')} )} {isAdmin && coStatus && coStatus.letsencrypt && ( - You have enabled Let's Encrypt for automatic HTTPS Certificate. You need to provide the configuration with an email address to use for Let's Encrypt in the configs. + {t('navigation.home.LetsEncryptEmailError')} )} @@ -301,7 +302,7 @@ const HomePage = () => { {isAdmin && coStatus && coStatus.LetsEncryptErrors && coStatus.LetsEncryptErrors.length > 0 && ( - There are errors with your Let's Encrypt configuration or one of your routes, please fix them as soon as possible: + {t('navigation.home.LetsEncryptError')} {coStatus.LetsEncryptErrors.map((err) => { return
- {err}
})} @@ -310,35 +311,31 @@ const HomePage = () => { {isAdmin && coStatus && coStatus.newVersionAvailable && ( - A new version of Cosmos is available! Please update to the latest version to get the latest features and bug fixes. + {t('navigation.home.newCosmosVersionError')} )} {isAdmin && coStatus && !coStatus.hostmode && config && ( - Your Cosmos server is not running in the docker host network mode. It is recommended that you migrate your install.
+ {t('navigation.home.cosmosNotDockerHostError')}
)} {isAdmin && coStatus && coStatus.needsRestart && ( - You have made changes to the configuration that require a restart to take effect. Please restart Cosmos to apply the changes. + {t('navigation.home.configChangeRequiresRestartError')} )} {isAdmin && coStatus && coStatus.domain && ( - You are using localhost or 0.0.0.0 as a hostname in the configuration. It is recommended that you use a domain name or an IP instead. + {t('navigation.home.localhostnotRecommendedError')} )} {isAdmin && coStatus && !coStatus.docker && ( - - Docker is not connected! Please check your docker connection.
- Did you forget to add
-v /var/run/docker.sock:/var/run/docker.sock
to your docker run command?
- if your docker daemon is running somewhere else, please add
-e DOCKER_HOST=...
to your docker run command. -
+ )}
@@ -349,7 +346,7 @@ const HomePage = () => { -
CPU
+
{t('global.CPU')}
-
-
@@ -363,9 +360,9 @@ const HomePage = () => { -
RAM
-
avail.: -
-
used: -
+
{t('global.RAM')}
+
{t('navigation.home.availRam')}: -
+
{t('navigation.home.usedRam')}: -
- @@ -394,9 +391,9 @@ const HomePage = () => { -
CPU
+
{t('global.CPU')}
{coStatus.CPU}
-
{coStatus.AVX ? "AVX Supported" : "No AVX Support"}
+
{coStatus.AVX ? t('navigation.home.Avx') : t('navigation.home.noAvx')}
{ -
RAM
-
avail.: {maxRAM}
-
used: {latestRAM}
+
{t('global.RAM')}
+
{t('navigation.home.availRam')}: {maxRAM}
+
{t('navigation.home.usedRam')}: {latestRAM}
{ - @@ -518,8 +515,8 @@ const HomePage = () => {
-

No Apps

-

You have no apps configured. Please add some apps in the configuration panel.

+

{t('navigation.home.noAppsTitle')}

+

{t('navigation.home.noApps')}

diff --git a/client/src/pages/market/listing.jsx b/client/src/pages/market/listing.jsx index c42a53e2..c63b2a35 100644 --- a/client/src/pages/market/listing.jsx +++ b/client/src/pages/market/listing.jsx @@ -15,6 +15,7 @@ import ResponsiveButton from "../../components/responseiveButton"; import { useClientInfos } from "../../utils/hooks"; import EditSourcesModal from "./sources"; import { PersistentCheckbox } from "../../components/persistentInput"; +import { useTranslation } from 'react-i18next'; function Screenshots({ screenshots }) { const aspectRatioContainerStyle = { @@ -60,6 +61,7 @@ function Showcases({ showcase, isDark, isAdmin }) { } function ShowcasesItem({ isDark, item, isAdmin }) { + const { t } = useTranslation(); return ( @@ -130,6 +132,7 @@ const gridAnim = { }; const MarketPage = () => { + const { t } = useTranslation(); const [apps, setApps] = useState([]); const [showcase, setShowcase] = useState([]); const theme = useTheme(); @@ -255,7 +258,7 @@ const MarketPage = () => { textDecoration: 'none', }}> @@ -278,16 +281,16 @@ const MarketPage = () => { {openedApp.appstore != 'cosmos-cloud' &&
- + - source: {openedApp.appstore} + {t('global.source')}: {openedApp.appstore}
}
-
repository: {openedApp.repository}
-
image: {openedApp.image}
-
compose: {openedApp.compose}
+
{t('navigation.market.repository')}: {openedApp.repository}
+
{t('navigation.market.image')}: {openedApp.image}
+
{t('navigation.market.compose')}: {openedApp.compose}
@@ -324,9 +327,9 @@ const MarketPage = () => { minHeight: 'calc(65vh - 80px)', padding: '24px', }}> -

Applications

+

{t('navigation.market.applicationsTitle')}

- { } - >Start ServApp + >{t('navigation.market.startServAppButton')} { }} /> - + {(!apps || !Object.keys(apps).length) && { + const { t } = useTranslation(); const [config, setConfig] = React.useState(null); const [open, setOpen] = React.useState(false); @@ -86,14 +88,14 @@ const EditSourcesModal = ({ onSave }) => { } if (source.Name === '') { - errors[`sources.${index}.Name`] = 'Name is required'; + errors[`sources.${index}.Name`] = t('global.name.validation'); } if (source.Url === '') { - errors[`sources.${index}.Url`] = 'URL is required'; + errors[`sources.${index}.Url`] = t('navigation.market.sources.urlRequiredValidation'); } if (source.Name === 'cosmos-cloud' || values.sources.filter((s) => s.Name === source.Name && !s.removed).length > 1) { - errors[`sources.${index}.Name`] = 'Name must be unique'; + errors[`sources.${index}.Name`] = t('navigation.market.sources.nameNotUniqueValidation'); } }); @@ -105,15 +107,15 @@ const EditSourcesModal = ({ onSave }) => { return (<> setOpen(false)} maxWidth="sm" fullWidth> - Edit Sources + {t('navigation.market.sourcesTitle')} {config &&
- This allows you to add additional 3rd party Cosmos app-markets to the market.
- To find new sources, start here + {t('navigation.market.newSources.additionalMarketsInfo')}
+ {t('navigation.market.newSources.additionalMarketsInfo.moreInfo')} {t('navigation.market.newSources.additionalMarketsInfo.href')}
{formik.values.sources && formik.values.sources .map((action, index) => { @@ -168,13 +170,13 @@ const EditSourcesModal = ({ onSave }) => { }, ]); }}> - Add Source + {t('navigation.market.sources.addSourceButton')}
- - + +
} @@ -185,7 +187,7 @@ const EditSourcesModal = ({ onSave }) => { variant="outlined" startIcon={} onClick={() => setOpen(true)} - >Sources + >{t('navigation.market.sources.editSourcesButton')} ); }; diff --git a/client/src/pages/newInstall/newInstall.jsx b/client/src/pages/newInstall/newInstall.jsx index e1e7de65..3d12b8d4 100644 --- a/client/src/pages/newInstall/newInstall.jsx +++ b/client/src/pages/newInstall/newInstall.jsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; - import * as Yup from 'yup'; +import { Trans, useTranslation } from 'react-i18next'; // material-ui import { Alert, Button, Checkbox, CircularProgress, FormControl, FormHelperText, Grid, Stack, Tooltip, Typography } from '@mui/material'; @@ -49,6 +49,7 @@ const debounce = (func, wait) => { const hostnameIsDomainReg = /^((?!localhost|\d+\.\d+\.\d+\.\d+)[a-zA-Z0-9\-]{1,63}\.)+[a-zA-Z]{2,63}$/ const NewInstall = () => { + const { t } = useTranslation(); const [activeStep, setActiveStep] = useState(0); const [status, setStatus] = useState(null); const [counter, setCounter] = useState(0); @@ -87,39 +88,38 @@ const NewInstall = () => { const getHTTPSOptions = (hostname) => { if(!hostname) { - return [["", "Set your hostname first"]]; + return [["", t('auth.hostnameInput')]]; } if(hostname.match(hostnameIsDomainReg)) { return [ - ["", "Select an option"], - ["LETSENCRYPT", "Use Let's Encrypt automatic HTTPS (recommended)"], - ["SELFSIGNED", "Generate self-signed certificate"], - ["PROVIDED", "Supply my own HTTPS certificate"], - ["DISABLED", "Use HTTP only (not recommended)"], + ["", t('newInstall.dbSelection.dbLabel')], + ["LETSENCRYPT", t('mgmt.config.security.encryption.httpsCertSelection.sslLetsEncryptChoice')], + ["SELFSIGNED", t('mgmt.config.security.encryption.httpsCertSelection.sslSelfSignedChoice')], + ["PROVIDED", t('mgmt.config.security.encryption.httpsCertSelection.sslProvidedChoice')], + ["DISABLED", t('mgmt.config.security.encryption.httpsCertSelection.sslDisabledChoice')], ] } else { return [ - ["", "Select an option"], - ["SELFSIGNED", "Generate self-signed certificate (recommended)"], - ["PROVIDED", "Supply my own HTTPS certificate"], - ["DISABLED", "Use HTTP only (not recommended)"], + ["", t('newInstall.dbSelection.dbLabel')], + ["SELFSIGNED", t('mgmt.config.security.encryption.httpsCertSelection.sslSelfSignedChoice')], + ["PROVIDED", t('mgmt.config.security.encryption.httpsCertSelection.sslProvidedChoice')], + ["DISABLED", t('mgmt.config.security.encryption.httpsCertSelection.sslDisabledChoice')], ] } } const steps = [ { - label: 'Welcome! 💖', + label: t('newInstall.welcomeTitle'), component:
- First of all, thanks a lot for trying out Cosmos! And Welcome to the setup wizard. - This wizard will guide you through the setup of Cosmos. It will take about 2-3 minutes and you will be ready to go. + {t('newInstall.welcomeText')}

- setCleanInstall(e.target.checked)} />Clean install (remove any existing config files) + setCleanInstall(e.target.checked)} />{t('newInstall.cleanInstallCheckbox')}

, @@ -128,19 +128,16 @@ const NewInstall = () => { } }, { - label: 'Docker 🐋 (step 1/4)', + label: t('newInstall.dockerTitle'), component:
- Cosmos is using docker to run applications. It is optional, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker. + {t('newInstall.whatIsCosmos')}
{status && (status.docker ? - Docker is installed and running. + {t('newInstall.dockerAvail')} : - - Docker is not connected! Please check your docker connection.
- Did you forget to add
-v /var/run/docker.sock:/var/run/docker.sock
to your docker run command?
- if your docker daemon is running somewhere else, please add
-e DOCKER_HOST=...
to your docker run command. + )} {(status && status.docker) ? (
@@ -151,28 +148,28 @@ const NewInstall = () => {
) : (<>
- Rechecking Docker Status... + {t('newInstall.dockerChecking')}
)}
, nextButtonLabel: () => { - return status && status.docker ? 'Next' : 'Skip'; + return status && status.docker ? t('global.next') : t('newInstall.skipAction'); } }, { - label: 'Database 🗄️ (step 2/4)', + label: t('newInstall.dbTitle'), component:
- Cosmos is using a MongoDB database to store all the data. It is optional, but Authentication as well as the UI will not work without a database. + {t('newInstall.dbText')}
{(status && status.database) ? - Database is connected. + {t('newInstall.dbConnected')} : <> - Database is not connected! + {t('newInstall.dbNotConnected')}
{
{pullRequest && { if(formik.values.DBMode === "DisableUserManagement") { setDatabaseEnable(false); @@ -223,19 +220,19 @@ const NewInstall = () => { {formik.values.DBMode === "Provided" && ( <> @@ -253,8 +250,8 @@ const NewInstall = () => { color="primary" disabled={formik.isSubmitting} fullWidth> - {formik.isSubmitting ? 'Loading' : ( - formik.values.DBMode === "DisableUserManagement" ? 'Disable' : 'Connect' + {formik.isSubmitting ? t('newInstall.loading') : ( + formik.values.DBMode === "DisableUserManagement" ? t('newInstall.usermgmt.disableButton') : t('mgmt.servapps.containers.terminal.connectButton') )} @@ -279,16 +276,14 @@ const NewInstall = () => {
)}
, nextButtonLabel: () => { - return (status && status.database) ? 'Next' : ''; + return (status && status.database) ? t('global.next') : ''; } }, { - label: 'HTTPS 🌐 (step 3/4)', + label: t('newInstall.httpsTitle'), component: (
- It is recommended to use Let's Encrypt to automatically provide HTTPS Certificates. - This requires a valid domain name pointing to this server. If you don't have one, you can select "Generate self-signed certificate" in the dropdown. - If you enable HTTPS, it will be effective after the next restart. + {t('newInstall.httpsText')}
{ }), Hostname: Yup.string().when('HTTPSCertificateMode', { is: "LETSENCRYPT", - then: Yup.string().required().matches(hostnameIsDomainReg, 'Let\'s Encrypt only accepts domain names'), + then: Yup.string().required().matches(hostnameIsDomainReg, t('newInstall.letsEncryptChoiceOnlyfqdnValidation')), otherwise: Yup.string().required() }), })} @@ -343,7 +338,7 @@ const NewInstall = () => { return res; } catch (error) { setStatus({ success: false }); - setErrors({ submit: "Please check you have filled all the inputs properly" }); + setErrors({ submit: t('newInstall.checkInputValidation') }); setSubmitting(false); } }}> @@ -352,8 +347,8 @@ const NewInstall = () => { { checkHost(e.target.value, setHostError, setHostIp); @@ -361,43 +356,33 @@ const NewInstall = () => { /> {formik.values.Hostname && (formik.values.Hostname.match(hostnameIsDomainReg) ? - You seem to be using a domain name.
- Let's Encrypt can automatically generate a certificate for you. +
: - You seem to be using an IP address or local domain.
- You can use automatic Self-Signed certificates. +
) } {formik.values.HTTPSCertificateMode === "LETSENCRYPT" && ( <> - - If you are using Cloudflare, make sure the DNS record is NOT set to Proxied (you should not see the orange cloud but a grey one). - Otherwise Cloudflare will not allow Let's Encrypt to verify your domain.
- Alternatively, you can also use the DNS challenge. -
+ {formik.values.DNSChallengeProvider && formik.values.DNSChallengeProvider != '' && ( - - You have enabled the DNS challenge. Make sure you have set the environment variables for your DNS provider. - You can enable it now, but make sure you have set up your API tokens accordingly before attempting to access - Cosmos after this installer. See doc here: https://go-acme.github.io/lego/dns/ - + )} { @@ -427,19 +412,18 @@ const NewInstall = () => { {hostError} } {hostIp && - This hostname is pointing to {hostIp}, check that it is your server IP! + } {formik.values.HTTPSCertificateMode === "LETSENCRYPT" && formik.values.UseWildcardCertificate && (!formik.values.DNSChallengeProvider || formik.values.DNSChallengeProvider == '') && ( - You have enabled wildcard certificates with Let's Encrypt. This only works if you use the DNS challenge! - Please edit the DNS Provider text input. + {t('newInstall.wildcardLetsEncryptError')} )} {(formik.values.HTTPSCertificateMode === "LETSENCRYPT" || formik.values.HTTPSCertificateMode === "SELFSIGNED") && formik.values.Hostname && formik.values.Hostname.match(hostnameIsDomainReg) && ( )} @@ -448,21 +432,14 @@ const NewInstall = () => { {formik.values.HTTPSCertificateMode != "" && (formik.values.HTTPSCertificateMode != "DISABLED" || isDomain(formik.values.Hostname)) ? ( Allow insecure access via local IP   - - When HTTPS is used along side a domain, depending on your networking configuration, it is possible that your server is not receiving direct local connections.
- This option allows you to also access your Cosmos admin using your local IP address, like ip:port.
- You can already create ip:port URLs for your apps, but this will make them HTTP-only.}> + label={{t('mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessLabel')}   + }>
} name="allowHTTPLocalIPAccess" formik={formik} /> - {formik.values.allowHTTPLocalIPAccess && - This option is not recommended as it exposes your server to security risks on your local network.
- Your local network is safer than the internet, but not safe, as devices like IoTs, smart-TVs, smartphones or even your router can be compromised.
- If you want to have a secure offline / local-only access to a server that uses a domain name and HTTPS, use Constellation instead. -
} + {formik.values.allowHTTPLocalIPAccess && }
) : ""} {formik.errors.submit && ( @@ -478,8 +455,8 @@ const NewInstall = () => { color="primary" disabled={formik.isSubmitting || !formik.isValid} fullWidth> - {formik.isSubmitting ? 'Loading' : ( - formik.values.HTTPSCertificateMode === "DISABLE" ? 'Disable' : 'Update' + {formik.isSubmitting ? t('newInstall.loading') : ( + formik.values.HTTPSCertificateMode === "DISABLE" ? t('newInstall.usermgmt.disableButton') : t('global.update') )} @@ -490,15 +467,15 @@ const NewInstall = () => {
), nextButtonLabel: () => { - return (status && status.hostname != '0.0.0.0') ? 'Next' : ''; + return (status && status.hostname != '0.0.0.0') ? t('global.next') : ''; } }, { - label: 'Admin Account 🔑 (step 4/4)', + label: t('newInstall.adminAccountTitle'), component:
- Create a local admin account to manage your server. Email is optional and used for notifications and password recovery. + {t('newInstall.adminAccountText')}
{ }} validationSchema={Yup.object().shape({ // nickname cant be admin or root - nickname: Yup.string().required('Nickname is required').min(3).max(32) - .matches(/^(?!admin|root).*$/, 'Nickname cannot be admin or root'), - password: Yup.string().required('Password is required').min(8).max(128).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[~!@#$%\^&\*\(\)_\+=\-\{\[\}\]:;"'<,>\/])(?=.{9,})/, 'Password must contain 9 characters: at least 1 lowercase, 1 uppercase, 1 number, and 1 special character'), - email: Yup.string().email('Must be a valid email').max(255), - confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], 'Passwords must match'), + nickname: Yup.string().required(t('global.nicknameRequiredValidation')).min(3).max(32) + .matches(/^(?!admin|root).*$/, t('newInstall.setupUser.nicknameRootAdminNotAllowedValidation')), + password: Yup.string().required(t('auth.pwdRequired')).min(8).max(128).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[~!@#$%\^&\*\(\)_\+=\-\{\[\}\]:;"'<,>\/])(?=.{9,})/, 'Password must contain 9 characters: at least 1 lowercase, 1 uppercase, 1 number, and 1 special character'), + email: Yup.string().email(t('global.emailInvalidValidation')).max(255), + confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], t('newInstall.setupUser.passwordMustMatchValidation')), })} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); @@ -536,8 +513,8 @@ const NewInstall = () => { { /> @@ -573,7 +550,7 @@ const NewInstall = () => { color="primary" disabled={formik.isSubmitting || !formik.isValid} fullWidth> - {formik.isSubmitting ? 'Loading' : 'Create'} + {formik.isSubmitting ? t('newInstall.loading') : t('global.createAction')} @@ -587,15 +564,10 @@ const NewInstall = () => { } }, { - label: 'Finish 🎉', - component:
- Well done! You have successfully installed Cosmos. You can now login to your server using the admin account you created. - If you have changed the hostname, don't forget to use that URL to access your server after the restart. - If you have are running into issues, check the logs for any error messages and edit the file in the /config folder. - If you still don't manage, please join our Discord server and we'll be happy to help! -
, + label: t('newInstall.finishTitle'), + component:
]} />
, nextButtonLabel: () => { - return 'Apply and Restart'; + return t('newInstall.applyRestartAction'); } } ]; @@ -624,7 +596,7 @@ const NewInstall = () => { setActiveStep(activeStep - 1) }} disabled={activeStep <= 0} - >Back + >{t('global.backAction')}
diff --git a/client/src/pages/openid/openid-edit.jsx b/client/src/pages/openid/openid-edit.jsx index 39ab83ba..4fa259ac 100644 --- a/client/src/pages/openid/openid-edit.jsx +++ b/client/src/pages/openid/openid-edit.jsx @@ -14,8 +14,10 @@ import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes' import HostChip from '../../components/hostChip'; import { Formik, useFormik } from 'formik'; import * as yup from 'yup'; +import { useTranslation } from 'react-i18next'; const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSubmit }) => { + const { t } = useTranslation(); const [submitErrors, setSubmitErrors] = useState([]); const [newRoute, setNewRoute] = useState(null); @@ -29,8 +31,8 @@ const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSu redirect: clientConfig ? clientConfig.redirect : '', }} validationSchema={yup.object({ - id: yup.string().required('Required'), - redirect: yup.string().required('Required'), + id: yup.string().required(t('global.required')), + redirect: yup.string().required(t('global.required')), })} onSubmit={(values) => { onSubmit && onSubmit(values); @@ -38,7 +40,7 @@ const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSu > {(formik) => ( - {clientId ? clientId : "New client"} + {clientId ? clientId : t('mgmt.openid.newClientTitle')} {openNewModal && <> @@ -57,7 +59,7 @@ const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSu fullWidth id="redirect" name="redirect" - label="Redirect" + label={t('mgmt.openId.redirect')} value={formik.values.redirect} onChange={formik.handleChange} error={formik.touched.redirect && Boolean(formik.errors.redirect)} @@ -72,10 +74,10 @@ const OpenIdEditModal = ({ clientId, openNewModal, setOpenNewModal, config, onSu return
{err}
})} } - + + }}>{clientId ? t('global.edit') : t('global.createAction')} } diff --git a/client/src/pages/openid/openid-list.jsx b/client/src/pages/openid/openid-list.jsx index 517d4355..af74d4b1 100644 --- a/client/src/pages/openid/openid-list.jsx +++ b/client/src/pages/openid/openid-list.jsx @@ -34,8 +34,8 @@ import { useNavigate } from 'react-router'; import NewRouteCreate from '../config/routes/newRoute'; import { DeleteButton } from '../../components/delete'; import OpenIdEditModal from './openid-edit'; - import bcrypt from 'bcryptjs'; +import { useTranslation } from 'react-i18next'; const stickyButton = { position: 'fixed', @@ -53,6 +53,7 @@ function shorten(test) { } const OpenIdList = () => { + const { t } = useTranslation(); const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; const [config, setConfig] = React.useState(null); @@ -130,11 +131,11 @@ const OpenIdList = () => {    + }}>{t('global.refresh')}   + }}>{t('global.createAction')} {config && <> @@ -158,10 +159,10 @@ const OpenIdList = () => { /> {newSecret && setNewSecret(false)}> - New Secret + {t('mgmt.openId.newSecret')} - Secret has been updated. Please copy it now as it will not be shown again. + {t('mgmt.openId.secretUpdated')}
@@ -178,13 +179,13 @@ const OpenIdList = () => { - +
} }> - This is an experimental feature. It is recommended to use with caution. Please report any issue you find! + {t('mgmt.openId.experimentalWarning')} {clients && { }, { - title: 'Redirect URI', + title: t('mgmt.openId.redirectUri'), screenMin: 'sm', search: (r) => r.redirect, field: (r) => r.redirect, @@ -224,7 +225,7 @@ const OpenIdList = () => { title: '', clickable: true, field: (r, k) => <>    + }}>{t('mgmt.openId.resetSecret')}   deleteClient(event, k)} /> , }, diff --git a/client/src/pages/servapps/actionBar.jsx b/client/src/pages/servapps/actionBar.jsx index 5c35fc77..3d5074ed 100644 --- a/client/src/pages/servapps/actionBar.jsx +++ b/client/src/pages/servapps/actionBar.jsx @@ -4,6 +4,7 @@ import { CheckCircleOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOu import * as API from '../../api'; import LogsInModal from '../../components/logsInModal'; import DeleteModal from './deleteModal'; +import { useTranslation } from 'react-i18next'; const GetActions = ({ Id, @@ -21,6 +22,7 @@ const GetActions = ({ const isMiniMobile = useMediaQuery((theme) => theme.breakpoints.down('xsm')); const [pullRequest, setPullRequest] = React.useState(null); const [isUpdating, setIsUpdating] = React.useState(false); + const { t } = useTranslation(); const doTo = (action) => { @@ -63,7 +65,7 @@ const GetActions = ({ let actions = [ { - t: 'Update Available' + (isStack ? ', go the stack details to update' : ', Click to Update'), + t: t('mgmt.servapps.actionBar.update') + (isStack ? ', go the stack details to update' : ', Click to Update'), if: ['update_available'], es: {}} size={isMiniMobile ? 'medium' : 'large'}> @@ -73,7 +75,7 @@ const GetActions = ({ }, { - t: 'No Update Available. Click to Force Pull', + t: t('mgmt.servapps.actionBar.noUpdate'), if: ['update_not_available'], hideStack: true, e: {doTo('update')}} size={isMiniMobile ? 'medium' : 'large'}> @@ -81,42 +83,42 @@ const GetActions = ({ }, { - t: 'Start', + t: t('mgmt.servapps.actionBar.start'), if: ['exited', 'created'], e: {doTo('start')}} size={isMiniMobile ? 'medium' : 'large'}> }, { - t: 'Unpause', + t: t('mgmt.servapps.actionBar.unpause'), if: ['paused'], e: {doTo('unpause')}} size={isMiniMobile ? 'medium' : 'large'}> }, { - t: 'Pause', + t: t('mgmt.servapps.actionBar.pause'), if: ['running'], e: {doTo('pause')}} size={isMiniMobile ? 'medium' : 'large'}> }, { - t: 'Stop', + t: t('mgmt.servapps.actionBar.stop'), if: ['paused', 'restarting', 'running'], e: {doTo('stop')}} size={isMiniMobile ? 'medium' : 'large'} variant="outlined"> }, { - t: 'Restart', + t: t('mgmt.servapps.actionBar.restart'), if: ['exited', 'running', 'paused', 'created', 'restarting'], e: doTo('restart')} size={isMiniMobile ? 'medium' : 'large'}> }, { - t: 'Re-create', + t: t('mgmt.servapps.actionBar.recreate'), if: ['exited', 'running', 'paused', 'created', 'restarting'], hideStack: true, e: doTo('recreate')} color="error" size={isMiniMobile ? 'medium' : 'large'}> @@ -124,14 +126,14 @@ const GetActions = ({ }, { - t: 'Kill', + t: t('mgmt.servapps.actionBar.kill'), if: ['running', 'paused', 'created', 'restarting'], e: doTo('kill')} color="error" size={isMiniMobile ? 'medium' : 'large'}> }, { - t: 'Delete', + t: t('global.delete'), if: ['exited', 'created'], e: } @@ -140,7 +142,7 @@ const GetActions = ({ return <> {pullRequest && { refreshServApps(); setPullRequest(null); diff --git a/client/src/pages/servapps/containers/docker-compose.jsx b/client/src/pages/servapps/containers/docker-compose.jsx index 22c398e5..ec96eaa0 100644 --- a/client/src/pages/servapps/containers/docker-compose.jsx +++ b/client/src/pages/servapps/containers/docker-compose.jsx @@ -34,6 +34,7 @@ import cmp from 'semver-compare'; import { HostnameChecker, getHostnameFromName } from '../../../utils/routes'; import { CosmosContainerPicker } from '../../config/users/containerPicker'; import { randomString } from '../../../utils/indexs'; +import { useTranslation } from 'react-i18next'; function checkIsOnline() { API.isOnline().then((res) => { @@ -378,6 +379,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) = } const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaultName }) => { + const { t } = useTranslation(); const cleanDefaultName = defaultName && defaultName.replace(/\s/g, '-').replace(/[^a-zA-Z0-9-]/g, ''); const [step, setStep] = useState(0); const [isLoading, setIsLoading] = useState(false); @@ -645,7 +647,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul return <> setOpenModal(false)} fullWidth maxWidth={'sm'}> - {installer ? "Installation" : "Import Compose File"} + {installer ? t('mgmt.servapps.compose.installTitle') : t('mgmt.servapps.importComposeFileButton')} {step === 0 && !installer && <> @@ -669,7 +671,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul setDockerCompose(e.target.value)} @@ -687,7 +689,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul {ymlError}
- {!ymlError && (<>Choose your service name + {!ymlError && (<>{t('mgmt.servApps.newContainer.serviceNameInput')} setServiceName(e.target.value)} /> @@ -774,7 +776,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul return Object.keys(service).map((hostIndex) => { const hostname = service[hostIndex]; return <> - Choose URL for {hostname.name} + {t('mgmt.servApps.newContainer.chooseUrl')} {hostname.name}
{hostname.description}
{ @@ -787,7 +789,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul })} {service && service.services && Object.values(service.services).map((value) => { - return + return - + { return { - Type: value.volumes[k].type || (k.startsWith('/') ? 'bind' : 'volume'), + Type: value.volumes[k].type || (k.startsWith('/') ? t('mgmt.servapps.newContainer.volumes.bindInput') : t('global.volume')), Source: value.volumes[k].source || "", Target: value.volumes[k].target || "", } @@ -870,7 +872,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul {(installerInit && service.minVersion && isNewerVersion(service.minVersion)) ? }> - This service requires a newer version of Cosmos. Please update Cosmos to install this service. + {t('mgmt.servApps.newContainer.cosmosOutdatedError')} : (!isLoading && @@ -884,7 +886,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul setContext({}); setHostnames({}); setOverrides({}); - }}>Close + }}>{t('global.close')} )}
@@ -906,7 +908,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul variant={(installerInit ? "contained" : "outlined")} startIcon={(installerInit ? : )} > - {installerInit ? 'Install' : 'Import Compose File'} + {installerInit ? t('mgmt.servapps.compose.installButton') : t('mgmt.servapps.importComposeFileButton')} ; diff --git a/client/src/pages/servapps/containers/index.jsx b/client/src/pages/servapps/containers/index.jsx index c8445218..7086c877 100644 --- a/client/src/pages/servapps/containers/index.jsx +++ b/client/src/pages/servapps/containers/index.jsx @@ -19,8 +19,10 @@ import DockerTerminal from './terminal'; import ContainerMetrics from '../../dashboard/containerMetrics'; import EventExplorerStandalone from '../../dashboard/eventsExplorerStandalone'; import ContainerComposeEdit from './compose-editor'; +import { useTranslation } from 'react-i18next'; const ContainerIndex = () => { + const { t } = useTranslation(); const { containerName } = useParams(); const [container, setContainer] = React.useState(null); const [config, setConfig] = React.useState(null); @@ -53,23 +55,23 @@ const ContainerIndex = () => { isLoading={!container || !config} tabs={[ { - title: 'Overview', + title: t('mgmt.servapps.overview'), children: }, { - title: 'Logs', + title: t('mgmt.scheduler.list.action.logs'), children: }, { - title: 'Monitoring', + title: t('menu-items.navigation.monitoringTitle'), children: }, { - title: 'Events', + title: t('navigation.monitoring.eventsTitle'), children: }, { - title: 'Terminal', + title: t('mgmt.servapps.terminal'), children: }, { @@ -81,11 +83,11 @@ const ContainerIndex = () => { children: }, { - title: 'Network', + title: t('global.network'), children: }, { - title: 'Storage', + title: t('menu-items.management.storage'), children: }, ]} /> diff --git a/client/src/pages/servapps/containers/logs.jsx b/client/src/pages/servapps/containers/logs.jsx index 5c3269b6..ea36a037 100644 --- a/client/src/pages/servapps/containers/logs.jsx +++ b/client/src/pages/servapps/containers/logs.jsx @@ -3,8 +3,10 @@ import { Box, Button, Checkbox, CircularProgress, Input, Stack, TextField, Typog import * as API from '../../../api'; import LogLine from '../../../components/logLine'; import { useTheme } from '@emotion/react'; +import { useTranslation } from 'react-i18next'; const Logs = ({ containerInfo }) => { + const { t } = useTranslation(); const { Name, Config, NetworkSettings, State } = containerInfo; const containerName = Name; const [logs, setLogs] = useState([]); @@ -114,9 +116,9 @@ const Logs = ({ containerInfo }) => { { setHasScrolled(false); setSearchTerm(e.target.value); @@ -132,7 +134,7 @@ const Logs = ({ containerInfo }) => { setLastReceivedLogs(''); }} /> - Error Only + {t('mgmt.servApps.container.protocols.errorOnlyCheckbox')}
@@ -156,7 +158,7 @@ const Logs = ({ containerInfo }) => { fetchLogs(true, true); }} > - Refresh + {t('global.refresh')} diff --git a/client/src/pages/servapps/containers/network.jsx b/client/src/pages/servapps/containers/network.jsx index 824893a5..c90008f5 100644 --- a/client/src/pages/servapps/containers/network.jsx +++ b/client/src/pages/servapps/containers/network.jsx @@ -11,8 +11,10 @@ import PrettyTableView from '../../../components/tableView/prettyTableView'; import { NetworksColumns } from '../networks'; import NewNetworkButton from '../createNetwork'; import LinkContainersButton from '../linkContainersButton'; +import { useTranslation } from 'react-i18next'; const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange, OnConnect, OnDisconnect }) => { + const { t } = useTranslation(); const [networks, setNetworks] = React.useState([]); const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; @@ -133,25 +135,25 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
- + {containerInfo.State && containerInfo.State.Status !== 'running' && ( - This container is not running. Editing any settings will cause the container to start again. + {t('mgmt.servApps.networks.containerotRunningWarning')} )} {isForceSecure && ( - This container is forced to be secured. You cannot expose any ports to the internet directly, please create a URL in Cosmos instead. You also cannot connect it to the Bridge network. + {t('mgmt.servApps.networks.forcedSecurityWarning')} )} - +
{formik.values.ports.map((port, idx) => ( @@ -169,7 +171,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O { @@ -248,13 +250,13 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O variant="contained" color="primary" > - Update Ports + {t('mgmt.servApps.networks.updatePortsButton')} }
- + {networks && @@ -262,8 +264,8 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O const network = networks.find((n) => n.Name === networkName); if (!network) { return - You are connected to a network that has been removed: {networkName}. - Either re-create it or + {t('mgmt.servApps.networks.removedNetConnectedError')} {networkName}. + {t('mgmt.servApps.networks.removedNetConnectedEitherRecreate')} } @@ -304,7 +306,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O } }, - ...NetworksColumns(theme, isDark), + ...NetworksColumns(theme, isDark, t), { title: '', field: (r) => { @@ -315,7 +317,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O onClick={() => { isConnected ? disconnect(r.Name) : connect(r.Name); }}> - {isConnected ? 'Disconnect' : 'Connect'} + {isConnected ? t('mgmt.servapps.containers.terminal.disconnectButton') : t('mgmt.servapps.containers.terminal.connectButton')} ) } } diff --git a/client/src/pages/servapps/containers/newService.jsx b/client/src/pages/servapps/containers/newService.jsx index 2d9b38ea..4f5b61f0 100644 --- a/client/src/pages/servapps/containers/newService.jsx +++ b/client/src/pages/servapps/containers/newService.jsx @@ -21,6 +21,7 @@ import { smartDockerLogConcat, tryParseProgressLog } from '../../../utils/docker import { LoadingButton } from '@mui/lab'; import LogLine from '../../../components/logLine'; import Highlighter from '../../../components/third-party/Highlighter'; +import { useTranslation } from 'react-i18next'; import Editor from 'react-simple-code-editor'; import { highlight, languages } from 'prismjs/components/prism-core'; @@ -59,6 +60,7 @@ const preStyle = { } const NewDockerService = ({service, refresh, edit}) => { + const { t } = useTranslation(); const { containerName } = useParams(); const [container, setContainer] = React.useState(null); const [config, setConfig] = React.useState(null); @@ -109,7 +111,7 @@ const NewDockerService = ({service, refresh, edit}) => { } return
- + {!isDone && { className={edit ? '' : 'shinyButton'} loading={log.length && !isDone} startIcon={edit ? : } - >{edit ? 'Edit': 'Create'}} + >{edit ? t('global.edit'): t('global.createAction')}} {isDone && - Service Created! + {t('mgmt.servapps.container.compose.createServiceSuccess')} {installer && installer['post-install'] && installer['post-install'].map(m =>{ return {m.label} })} } - {edit && !isDone && log.length ? : null} + {edit && !isDone && log.length ? : null} {log.length ?
         {log.map((l) => {
@@ -139,7 +141,7 @@ const NewDockerService = ({service, refresh, edit}) => {
       
setDockerCompose(code)} highlight={code => highlight(code, isJSON ? languages.json : languages.yaml)} padding={10} diff --git a/client/src/pages/servapps/containers/newServiceForm.jsx b/client/src/pages/servapps/containers/newServiceForm.jsx index 1a3e3255..6cd2d815 100644 --- a/client/src/pages/servapps/containers/newServiceForm.jsx +++ b/client/src/pages/servapps/containers/newServiceForm.jsx @@ -18,8 +18,10 @@ import VolumeContainerSetup from './volumes'; import DockerTerminal from './terminal'; import NewDockerService from './newService'; import RouteManagement from '../../config/routes/routeman'; +import { useTranslation } from 'react-i18next'; const NewDockerServiceForm = () => { + const { t } = useTranslation(); const [currentTab, setCurrentTab] = React.useState(0); const [maxTab, setMaxTab] = React.useState(0); const [config, setConfig] = React.useState(null); @@ -102,7 +104,7 @@ const NewDockerServiceForm = () => { setCurrentTab(currentTab - 1); }} > - Previous + {t('newInstall.previousButton')} @@ -122,7 +124,7 @@ const NewDockerServiceForm = () => { -
Start New Servapp
+
{t('mgmt.servApp.newServAppButton')}
{ { } setContainerInfo(newValues); }} - />Create a URL to access this ServApp + />{t('mgmt.servApp.url')} {containerInfo.CreateRoute && { />}{nav()}
}, { - title: 'Network', + title: t('global.network'), disabled: maxTab < 1, children: { const newValues = { @@ -280,7 +282,7 @@ const NewDockerServiceForm = () => { }}/>{nav()} }, { - title: 'Storage', + title: t('menu-items.management.storage'), disabled: maxTab < 1, children: { const newValues = { @@ -300,7 +302,7 @@ const NewDockerServiceForm = () => { }} />{nav()} }, { - title: 'Review & Start', + title: t('mgmt.servApp.newContainer.reviewStartButton'), disabled: maxTab < 1, children: {nav()} } diff --git a/client/src/pages/servapps/containers/overview.jsx b/client/src/pages/servapps/containers/overview.jsx index 7bb0c261..ed105266 100644 --- a/client/src/pages/servapps/containers/overview.jsx +++ b/client/src/pages/servapps/containers/overview.jsx @@ -11,6 +11,7 @@ import GetActions from '../actionBar'; import { ServAppIcon } from '../../../utils/servapp-icon'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; import UploadButtons from '../../../components/fileUpload'; +import { useTranslation } from 'react-i18next'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', @@ -19,6 +20,7 @@ const info = { } const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, selfName }) => { + const { t } = useTranslation(); const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); const [openModal, setOpenModal] = React.useState(false); const [openRestartModal, setOpenRestartModal] = React.useState(false); @@ -79,13 +81,13 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s
{({ - "created": , - "restarting": , - "running": , - "removing": , - "paused": , - "exited": , - "dead": , + "created": , + "restarting": , + "running": , + "removing": , + "paused": , + "exited": , + "dead": , })[State.Status]}
{containerInfo.State.Status !== 'running' && ( - This container is not running. Editing any settings will cause the container to start again. + {t('mgmt.servApps.notRunningWarning')} )} - Image + {t('mgmt.servApps.container.overview.imageTitle')}
{Image}
ID
{containerInfo.Id}
- IP Address + {t('mgmt.servApps.container.overview.ipAddressTitle')}
{IPAddress}
- Health + {t('mgmt.servApps.container.overview.healthTitle')}
{healthStatus}
- Settings {State.Status !== 'running' ? '(Start container to edit)' : ''} + {t('mgmt.servApps.container.overview.settingsTitle')} {State.Status !== 'running' ? t('mgmt.servApps.startToEditInfo') : ''} {/* Auto Update Container + /> {t('mgmt.servApps.autoUpdateCheckbox')} URLs
@@ -181,7 +183,7 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s })}
} @@ -193,7 +195,7 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s }} />
- Monitoring + {t('menu-items.navigation.monitoringTitle')}
{ const labels = {}; @@ -69,6 +70,7 @@ const DockerContainerSetup = ({ newContainer, OnForceSecure, }) => { + const { t } = useTranslation(); const [pullRequest, setPullRequest] = useState(null); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); @@ -77,7 +79,7 @@ const DockerContainerSetup = ({ const wrapCard = (children) => { if (noCard) return children; - return {children}; + return {children}; }; const initialValues = useMemo(() => { @@ -121,10 +123,10 @@ const DockerContainerSetup = ({ (values) => { const errors = {}; if (!values.image) { - errors.image = "Required"; + errors.image = t('global.required'); } if (!values.name && newContainer) { - errors.name = "Required"; + errors.name = t('global.required'); } // env keys and labels key mustbe unique const envKeys = values.envVars.map((envVar) => envVar.key); @@ -132,10 +134,10 @@ const DockerContainerSetup = ({ const uniqueEnvKeysKeys = [...new Set(envKeys)]; const uniqueLabelKeys = [...new Set(labelKeys)]; if (uniqueEnvKeysKeys.length !== envKeys.length) { - errors.submit = "Environment Variables must be unique"; + errors.submit = t('mgmt.servapps.newContainer.env.keyNotUniqueError'); } if (uniqueLabelKeys.length !== labelKeys.length) { - errors.submit = "Labels must be unique"; + errors.submit = t('mgmt.servapps.newContainer.label.labelNotUniqueError'); } OnChange && OnChange(containerInfoFrom(values)); return errors; @@ -195,7 +197,7 @@ const DockerContainerSetup = ({ {pullRequest && ( { setPullRequest(null); setLatestImage(formik.values.image); @@ -214,8 +216,7 @@ const DockerContainerSetup = ({ severity="warning" style={{ marginBottom: "15px" }} > - This container is not running. Editing any settings will - cause the container to start again. + {t('mgmt.servApps.volumes.containerNotRunningWarning')} )} @@ -224,33 +225,33 @@ const DockerContainerSetup = ({ {newContainer && ( )} {OnForceSecure && ( @@ -259,7 +260,7 @@ const DockerContainerSetup = ({ type="checkbox" as={FormControlLabel} control={} - label={"Force secure container"} + label={t('mgmt.servapps.newContainer.forceSecureCheckbox.forceSecureLabel')} checked={ containerInfo.Config.Labels.hasOwnProperty( "cosmos-force-network-secured" @@ -277,7 +278,7 @@ const DockerContainerSetup = ({ )} - + {formik.values.envVars.map((envVar, idx) => ( @@ -285,7 +286,7 @@ const DockerContainerSetup = ({ @@ -295,7 +296,7 @@ const DockerContainerSetup = ({ name={`envVars.${idx}.value`} onChange={formik.handleChange} fullWidth - label="Value" + label={t('mgmt.servapps.newContainer.env.envValueInput.envValueLabel')} value={envVar.value} /> @@ -327,11 +328,11 @@ const DockerContainerSetup = ({ }} startIcon={} > - Add + {t('global.addAction')} - + {formik.values.labels.map((label, idx) => ( @@ -339,7 +340,7 @@ const DockerContainerSetup = ({ @@ -347,7 +348,7 @@ const DockerContainerSetup = ({ } > - Add + {t('global.addAction')} - + {formik.values.devices.map((device, idx) => ( @@ -392,7 +393,7 @@ const DockerContainerSetup = ({ @@ -400,7 +401,7 @@ const DockerContainerSetup = ({ } > - Add + {t('global.addAction')} @@ -454,9 +455,7 @@ const DockerContainerSetup = ({ severity="warning" style={{ marginBottom: "15px" }} > - You have updated the image. Clicking the button below - will pull the new image, and then only can you update - the container. + {t('mgmt.servapps.newContainer.imageUpdateWarning')} )} {formik.values.image !== latestImage - ? "Pull New Image" - : "Update Container"} + ? t('mgmt.servapps.newContainer.pullImageButton') + : t('mgmt.servapps.newContainer.updateContainerButton')} diff --git a/client/src/pages/servapps/containers/terminal.jsx b/client/src/pages/servapps/containers/terminal.jsx index a583e0f2..b3001f5d 100644 --- a/client/src/pages/servapps/containers/terminal.jsx +++ b/client/src/pages/servapps/containers/terminal.jsx @@ -5,12 +5,14 @@ import * as API from '../../../api'; import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material'; import { ApiOutlined, SendOutlined } from '@ant-design/icons'; import ResponsiveButton from '../../../components/responseiveButton'; +import { useTranslation } from 'react-i18next'; import { Terminal } from '@xterm/xterm' import '@xterm/xterm/css/xterm.css' import { FitAddon } from '@xterm/addon-fit'; const DockerTerminal = ({containerInfo, refresh}) => { + const { t } = useTranslation(); const { Name, Config, NetworkSettings, State } = containerInfo; const isInteractive = Config.Tty; const theme = useTheme(); @@ -76,14 +78,14 @@ const DockerTerminal = ({containerInfo, refresh}) => { setIsConnected(false); let terminalBoldRed = '\x1b[1;31m'; let terminalReset = '\x1b[0m'; - terminal.write(terminalBoldRed + 'Disconnected from ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset); + terminal.write(terminalBoldRed + t('mgmt.servapps.containers.terminal.disconnectedFromText') + (newProc ? 'shell' : t('mgmt.servapps.containers.terminal.mainprocessTty')) + '\r\n' + terminalReset); }; ws.current.onopen = () => { setIsConnected(true); let terminalBoldGreen = '\x1b[1;32m'; let terminalReset = '\x1b[0m'; - terminal.write(terminalBoldGreen + 'Connected to ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset); + terminal.write(terminalBoldGreen + t('mgmt.servapps.containers.terminal.connectedToText') + (newProc ? 'shell' : t('mgmt.servapps.containers.terminal.mainprocessTty')) + '\r\n' + terminalReset); // focus terminal terminal.focus(); }; @@ -187,9 +189,8 @@ const DockerTerminal = ({containerInfo, refresh}) => { }}> {(!isInteractive) && ( - This container is not interactive. - If you want to connect to the main process, - + {t('mgmt.servapps.containers.terminal.terminalNotInteractiveWarning')} + )}
{ }
{isConnected ? (<> - + ) : <> - + onClick={() => connect(false)}>{t('mgmt.servapps.containers.terminal.connectButton')} + } diff --git a/client/src/pages/servapps/containers/volumes.jsx b/client/src/pages/servapps/containers/volumes.jsx index 17664c55..4778e19d 100644 --- a/client/src/pages/servapps/containers/volumes.jsx +++ b/client/src/pages/servapps/containers/volumes.jsx @@ -19,6 +19,7 @@ import * as API from "../../../api"; import { LoadingButton } from "@mui/lab"; import PrettyTableView from "../../../components/tableView/prettyTableView"; import ResponsiveButton from "../../../components/responseiveButton"; +import { useTranslation } from 'react-i18next'; const VolumeContainerSetup = ({ noCard, @@ -28,6 +29,7 @@ const VolumeContainerSetup = ({ newContainer, OnChange, }) => { + const { t } = useTranslation(); const [volumes, setVolumes] = React.useState([]); const theme = useTheme(); @@ -53,7 +55,7 @@ const VolumeContainerSetup = ({ const wrapCard = (children) => { if (noCard) return children; - return {children}; + return {children}; }; const initialValues = useMemo(() => { @@ -81,7 +83,7 @@ const VolumeContainerSetup = ({ }); const unique = [...new Set(volumes)]; if (unique.length !== volumes.length) { - errors.submit = "Mounts must have unique targets"; + errors.submit = t('mgmt.servapps.newContainer.volumes.mountNotUniqueError'); } OnChange && OnChange(values, volumes); return errors; @@ -145,8 +147,7 @@ const VolumeContainerSetup = ({ severity="warning" style={{ marginBottom: "15px" }} > - This container is not running. Editing any settings will - cause the container to start again. + {t('mgmt.servApps.volumes.containerNotRunningWarning')} )} @@ -176,7 +177,7 @@ const VolumeContainerSetup = ({ ]); }} > - New Mount Point + {t('mgmt.servapps.newContainer.volumes.newMountButton')} , ]} columns={[ @@ -202,14 +203,14 @@ const VolumeContainerSetup = ({ name={`volumes[${k}].Type`} onChange={formik.handleChange} > - Bind - Volume + {t('mgmt.servapps.newContainer.volumes.bindInput')} + {t('global.volume')}
), }, { - title: "Source", + title: t('global.source'), field: (r, k) => (
(
- Unmount + {t('global.unmount')} ); }, @@ -344,7 +345,7 @@ const VolumeContainerSetup = ({ variant="contained" color="primary" > - Update Volumes + {t('mgmt.servapps.newContainer.volumes.updateVolumesButton')} )} diff --git a/client/src/pages/servapps/createNetwork.jsx b/client/src/pages/servapps/createNetwork.jsx index b33c17ab..3fc082ed 100644 --- a/client/src/pages/servapps/createNetwork.jsx +++ b/client/src/pages/servapps/createNetwork.jsx @@ -14,8 +14,11 @@ import * as Yup from 'yup'; import * as API from '../../api'; import { CosmosCheckbox } from '../config/users/formShortcuts'; import ResponsiveButton from '../../components/responseiveButton'; +import { useTranslation } from 'react-i18next'; + const NewNetworkButton = ({ fullWidth, refresh }) => { + const { t } = useTranslation(); const [isOpened, setIsOpened] = React.useState(false); const formik = useFormik({ @@ -27,11 +30,11 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { subnet: '', }, validationSchema: Yup.object({ - name: Yup.string().required('Required'), - driver: Yup.string().required('Required'), + name: Yup.string().required(t('global.required')), + driver: Yup.string().required(t('global.required')), parentInterface: Yup.string().when('driver', { is: 'macvlan', - then: Yup.string().required('Parent interface is required for MACVLAN') + then: Yup.string().required(t('mgmt.servApps.createNetwork.parentReqForMacvlan')) }), }), onSubmit: (values, { setErrors, setStatus, setSubmitting }) => { @@ -54,7 +57,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { <> setIsOpened(false)}> - New Network + {t('mgmt.servapps.networks.list.newNetwork')} @@ -63,7 +66,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { fullWidth id="name" name="name" - label="Name" + label={t('global.nameTitle')} value={formik.values.name} onChange={formik.handleChange} error={formik.touched.name && Boolean(formik.errors.name)} @@ -77,21 +80,21 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { error={formik.touched.driver && Boolean(formik.errors.driver)} style={{ marginBottom: '16px' }} > - Driver + {t('global.driver')} @@ -99,7 +102,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { fullWidth id="subnet" name="subnet" - label="Subnet (optional)" + label={t('mgmt.servapps.networks.list.subnet')} value={formik.values.subnet} onChange={formik.handleChange} error={formik.touched.subnet && Boolean(formik.errors.subnet)} @@ -112,7 +115,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { fullWidth id="parentInterface" name="parentInterface" - label="Parent Interface" + label={t('mgmt.servapps.networks.list.parentIf')} value={formik.values.parentInterface} onChange={formik.handleChange} error={formik.touched.parentInterface && Boolean(formik.errors.parentInterface)} @@ -123,7 +126,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { @@ -136,13 +139,13 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { - + - Create + {t('global.createAction')} @@ -152,7 +155,7 @@ const NewNetworkButton = ({ fullWidth, refresh }) => { onClick={() => setIsOpened(true)} startIcon={} > - New Network + {t('mgmt.servapps.networks.list.newNetwork')} ); diff --git a/client/src/pages/servapps/createVolumes.jsx b/client/src/pages/servapps/createVolumes.jsx index 18bea7bc..f9b89a43 100644 --- a/client/src/pages/servapps/createVolumes.jsx +++ b/client/src/pages/servapps/createVolumes.jsx @@ -19,10 +19,11 @@ import { import { PlusCircleOutlined } from '@ant-design/icons'; import { LoadingButton } from '@mui/lab'; import * as API from '../../api'; +import { useTranslation } from 'react-i18next'; const NewVolumeButton = ({ fullWidth, refresh }) => { + const { t } = useTranslation(); const [isOpened, setIsOpened] = useState(false); - const formik = useFormik({ initialValues: { name: '', @@ -52,7 +53,7 @@ const NewVolumeButton = ({ fullWidth, refresh }) => { <> setIsOpened(false)}> - New Volume + {t('mgmt.servApps.volumes.newVolumeTitle')} @@ -73,18 +74,18 @@ const NewVolumeButton = ({ fullWidth, refresh }) => { error={formik.touched.driver && Boolean(formik.errors.driver)} style={{ marginBottom: '16px' }} > - Driver + {t('global.driver')} @@ -96,12 +97,12 @@ const NewVolumeButton = ({ fullWidth, refresh }) => { )} - + - Create + {t('global.createAction')} @@ -111,7 +112,7 @@ const NewVolumeButton = ({ fullWidth, refresh }) => { onClick={() => setIsOpened(true)} startIcon={} > - New Volume + {t('mgmt.servApps.volumes.newVolumeTitle')} ); diff --git a/client/src/pages/servapps/deleteModal.jsx b/client/src/pages/servapps/deleteModal.jsx index c500bd57..d5f20afd 100644 --- a/client/src/pages/servapps/deleteModal.jsx +++ b/client/src/pages/servapps/deleteModal.jsx @@ -6,8 +6,10 @@ import LogsInModal from '../../components/logsInModal'; import { DeleteButton } from '../../components/delete'; import { CosmosCheckbox } from '../config/users/formShortcuts'; import { getContaienrsJobs, getContainersRoutes } from '../../utils/routes'; +import { useTranslation } from 'react-i18next'; const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config}) => { + const { t } = useTranslation(); const [isOpen, setIsOpen] = React.useState(false); const [confirmDelete, setConfirmDelete] = React.useState(false); const [failed, setFailed] = React.useState([]); @@ -211,16 +213,16 @@ const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config} return <> {isOpen && <> {refreshServApps() ; setIsOpen(false)}}> - Delete Service + {t('mgmt.servApps.container.deleteService')}
{isDeleting &&
- Deletion status: + {t('mgmt.servApps.container.deleteServiceStatus')}
} {!isDeleting &&
- Select what you wish to delete: + {t('mgmt.servApps.container.selectWhatToDelete')}
}
{containers.map((container) => { @@ -230,38 +232,38 @@ const DeleteModal = ({Ids, containers, refreshServApps, setIsUpdatingId, config} })} {networks.map((network) => { return (!isDeleting || (!ignored.includes(network + "-network"))) &&
- Network {network} + {t('global.network')} {network}
})} {volumes.map((mount) => { return (!isDeleting || (!ignored.includes(mount + "-volume"))) &&
- Volume {mount} + {t('global.volume')} {mount}
})} {routes.map((route) => { return (!isDeleting || (!ignored.includes(route + "-route"))) &&
- Route {route} + {t('mgmt.servApps.container.delete.route')} {route}
})} {cronJobs.map((job) => { return (!isDeleting || (!ignored.includes(job + "-job"))) &&
- Cron Job {job} + {t('mgmt.servApps.container.delete.cronjob')} {job}
})}
{!isDeleting && - + + }}>{t('global.delete')} } {isDeleting && + }}>{t('mgmt.servApps.container.delete.done')} }
} diff --git a/client/src/pages/servapps/exposeModal.jsx b/client/src/pages/servapps/exposeModal.jsx index 5ba25daa..36e88cc9 100644 --- a/client/src/pages/servapps/exposeModal.jsx +++ b/client/src/pages/servapps/exposeModal.jsx @@ -4,8 +4,10 @@ import { Alert } from '@mui/material'; import RouteManagement from '../config/routes/routeman'; import { ValidateRoute, getFaviconURL, sanitizeRoute, getContainersRoutes, getHostnameFromName } from '../../utils/routes'; import * as API from '../../api'; +import { useTranslation } from 'react-i18next'; const ExposeModal = ({ openModal, setOpenModal, config, updateRoutes, container }) => { + const { t } = useTranslation(); const [submitErrors, setSubmitErrors] = useState([]); const [newRoute, setNewRoute] = useState(null); @@ -16,13 +18,13 @@ const ExposeModal = ({ openModal, setOpenModal, config, updateRoutes, container } return setOpenModal(false)}> - Expose ServApp + {t('mgmt.servApp.container.urls.exposeTitle')} {openModal && <>
- Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL. + {t('mgmt.servApp.container.urls.exposeText')}
{err}
})}
} - + + }}>{t('global.confirmAction')} }
diff --git a/client/src/pages/servapps/index.jsx b/client/src/pages/servapps/index.jsx index a5912fbb..eda6b24d 100644 --- a/client/src/pages/servapps/index.jsx +++ b/client/src/pages/servapps/index.jsx @@ -12,24 +12,26 @@ import ServApps from './servapps'; import VolumeManagementList from './volumes'; import NetworkManagementList from './networks'; import { useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; const ServappsIndex = () => { + const { t } = useTranslation(); const { stack } = useParams(); return
{!stack && , path: 'containers' }, { - title: 'Volumes', + title: t('mgmt.servapps.networks.volumes'), children: , path: 'volumes' }, { - title: 'Networks', + title: t('global.networks'), children: , path: 'networks' }, diff --git a/client/src/pages/servapps/linkContainersButton.jsx b/client/src/pages/servapps/linkContainersButton.jsx index de8e526f..dbda83d7 100644 --- a/client/src/pages/servapps/linkContainersButton.jsx +++ b/client/src/pages/servapps/linkContainersButton.jsx @@ -12,6 +12,7 @@ import { useEffect, useState } from 'react'; import { LoadingButton } from '@mui/lab'; import { FormikProvider, useFormik } from 'formik'; import * as Yup from 'yup'; +import { useTranslation } from 'react-i18next'; import * as API from '../../api'; import { CosmosCheckbox } from '../config/users/formShortcuts'; @@ -20,6 +21,7 @@ import { CosmosContainerPicker } from '../config/users/containerPicker'; import { randomString } from '../../utils/indexs'; const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContainer, OnConnect }) => { + const { t } = useTranslation(); const [isOpened, setIsOpened] = useState(false); const formik = useFormik({ initialValues: { @@ -67,7 +69,7 @@ const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContaine <> setIsOpened(false)}> - Link with container + {t('mgmt.servApps.container.network.linkContainerTitle')} @@ -89,12 +91,12 @@ const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContaine - + - Link Containers + {t('mgmt.servApps.container.network.linkContainerButton')} @@ -104,7 +106,7 @@ const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContaine onClick={() => setIsOpened(true)} startIcon={} > - Link Containers + {t('mgmt.servApps.container.network.linkContainerButton')} ); diff --git a/client/src/pages/servapps/networks.jsx b/client/src/pages/servapps/networks.jsx index 58e420c3..6df9f957 100644 --- a/client/src/pages/servapps/networks.jsx +++ b/client/src/pages/servapps/networks.jsx @@ -6,10 +6,11 @@ import { useEffect, useState } from 'react'; import * as API from '../../api'; import PrettyTableView from '../../components/tableView/prettyTableView'; import NewNetworkButton from './createNetwork'; +import { useTranslation } from 'react-i18next'; -export const NetworksColumns = (theme, isDark) => [ +export const NetworksColumns = (theme, isDark, t) => [ { - title: 'Network Name', + title: t('mgmt.servapps.networks.list.networkName'), field: (r) =>
{r.Name}

{r.Driver} driver
@@ -17,7 +18,7 @@ export const NetworksColumns = (theme, isDark) => [ search: (r) => r.Name, }, { - title: 'Properties', + title: t('mgmt.servapps.networks.list.networkproperties'), screenMin: 'md', field: (r) => ( @@ -29,23 +30,24 @@ export const NetworksColumns = (theme, isDark) => [ ), }, { - title: 'IPAM gateway / mask', + title: t('mgmt.servapps.networks.list.networkIpam'), screenMin: 'lg', field: (r) => r.IPAM.Config ? r.IPAM.Config.map((config, index) => (
{config.Gateway}
{config.Subnet}
- )) : 'No Ip', + )) : t('mgmt.servapps.networks.list.networkNoIp'), }, { - title: 'Created At', + title: t('global.createdAt'), screenMin: 'lg', field: (r) => r.Created ? new Date(r.Created).toLocaleString() : '-', }, ]; const NetworkManagementList = () => { + const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState(null); const [tryDelete, setTryDelete] = useState(null); @@ -69,7 +71,7 @@ const NetworkManagementList = () => { <> @@ -83,7 +85,7 @@ const NetworkManagementList = () => { onRowClick={() => { }} getKey={(r) => r.Id} columns={[ - ...NetworksColumns(theme, isDark), + ...NetworksColumns(theme, isDark, t), { title: '', clickable: true, @@ -108,7 +110,7 @@ const NetworkManagementList = () => { } }} > - {tryDelete === r.Id ? "Really?" : "Delete"} + {tryDelete === r.Id ? t('global.confirmDeletion') : t('global.delete')} ), diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index 5fd47522..9dc7196b 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -22,6 +22,7 @@ import { ServAppIcon } from '../../utils/servapp-icon'; import MiniPlotComponent from '../dashboard/components/mini-plot'; import { DownloadFile } from '../../api/downloadButton'; import { useTheme } from '@mui/material/styles'; +import { useTranslation } from 'react-i18next'; const Item = styled(Paper)(({ theme }) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', @@ -48,6 +49,7 @@ const noOver = { } const ServApps = ({stack}) => { + const { t } = useTranslation(); const [servApps, setServApps] = useState([]); const [isUpdating, setIsUpdating] = useState({}); const [search, setSearch] = useState(""); @@ -246,7 +248,7 @@ const ServApps = ({stack}) => { {stack && }>Back } - @@ -259,18 +261,18 @@ const ServApps = ({stack}) => { /> } onClick={() => { refreshServApps(); - }}>Refresh + }}>{t('global.refresh')} {!stack && <> } - >Start ServApp + >{t('navigation.market.startServAppButton')} } @@ -279,7 +281,7 @@ const ServApps = ({stack}) => { {updatesAvailable && updatesAvailable.length && - Update are available for {Object.keys(updatesAvailable).join(', ')} + {t('mgmt.servapps.updatesAvailableFor')} {Object.keys(updatesAvailable).join(', ')} } {servApps && Object.values(servAppsStacked) @@ -299,13 +301,13 @@ const ServApps = ({stack}) => { { ({ - "created": , - "restarting": , - "running": , - "removing": , - "paused": , - "exited": , - "dead": , + "created": , + "restarting": , + "running": , + "removing": , + "paused": , + "exited": , + "dead": , })[app.state] } @@ -361,7 +363,7 @@ const ServApps = ({stack}) => {
- Networks + {t('global.networks')} {app.networkSettings.Networks && Object.keys(app.networkSettings.Networks).map((network) => { @@ -371,7 +373,7 @@ const ServApps = ({stack}) => { - URLs + {t('menu-items.management.urls')} {getContainersRoutes(config, app.name.replace('/', '')).map((route) => { @@ -379,7 +381,7 @@ const ServApps = ({stack}) => { })} {/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */} } @@ -438,7 +440,7 @@ const ServApps = ({stack}) => { refreshServApps(); }) }} - /> Auto Update Container + /> {t('mgmt.servApps.autoUpdateCheckbox')} } @@ -447,8 +449,8 @@ const ServApps = ({stack}) => { "cosmos.system.docker.cpu." + app.name.replace('/', ''), "cosmos.system.docker.ram." + app.name.replace('/', ''), ]} labels={{ - ["cosmos.system.docker.cpu." + app.name.replace('/', '')]: "CPU", - ["cosmos.system.docker.ram." + app.name.replace('/', '')]: "RAM" + ["cosmos.system.docker.cpu." + app.name.replace('/', '')]: t('global.CPU'), + ["cosmos.system.docker.ram." + app.name.replace('/', '')]: t('global.RAM') }}/>
@@ -458,7 +460,7 @@ const ServApps = ({stack}) => { `/cosmos-ui/servapps/containers/${app.name.replace('/', '')}` }>
diff --git a/client/src/pages/servapps/volumes.jsx b/client/src/pages/servapps/volumes.jsx index 51fc0318..05f0b56e 100644 --- a/client/src/pages/servapps/volumes.jsx +++ b/client/src/pages/servapps/volumes.jsx @@ -14,8 +14,10 @@ import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes' import HostChip from '../../components/hostChip'; import PrettyTableView from '../../components/tableView/prettyTableView'; import NewVolumeButton from './createVolumes'; +import { useTranslation } from 'react-i18next'; const VolumeManagementList = () => { + const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState(null); const [tryDelete, setTryDelete] = useState(null); @@ -39,7 +41,7 @@ const VolumeManagementList = () => { <> @@ -54,7 +56,7 @@ const VolumeManagementList = () => { ]} columns={[ { - title: 'Volume Name', + title: t('mgmt.servapps.volumes.volumeName'), field: (r) =>
{r.Name}

{r.Mountpoint}
@@ -62,17 +64,17 @@ const VolumeManagementList = () => { search: (r) => r.Name, }, { - title: 'Driver', + title: t('global.driver'), screenMin: 'lg', field: (r) => r.Driver, }, { - title: 'Scope', + title: t('mgmt.servapps.volumes.list.ScopeTitle'), screenMin: 'lg', field: (r) => r.Scope, }, { - title: 'Created At', + title: t('global.createdAt'), screenMin: 'lg', field: (r) => new Date(r.CreatedAt).toLocaleString(), }, @@ -100,7 +102,7 @@ const VolumeManagementList = () => { } }} > - {tryDelete === r.Name ? "Really?" : "Delete"} + {tryDelete === r.Name ? t('global.confirmDeletion') : t('global.delete')} ), diff --git a/client/src/pages/storage/FormatModal.jsx b/client/src/pages/storage/FormatModal.jsx index 8185e98e..1304a47d 100644 --- a/client/src/pages/storage/FormatModal.jsx +++ b/client/src/pages/storage/FormatModal.jsx @@ -9,8 +9,10 @@ import DialogTitle from '@mui/material/DialogTitle'; import { LoadingButton } from '@mui/lab'; import { useFormik, FormikProvider } from 'formik'; import * as Yup from 'yup'; +import { useTranslation } from 'react-i18next'; const FormatModal = ({ cb, OnClose }) => { + const { t } = useTranslation(); const formik = useFormik({ initialValues: { password: '', @@ -37,7 +39,7 @@ const FormatModal = ({ cb, OnClose }) => { <> OnClose()}> - Format Disk + {t('mgmt.storage.formatDiskTitle')} @@ -49,13 +51,13 @@ const FormatModal = ({ cb, OnClose }) => { error={formik.touched.format && Boolean(formik.errors.format)} style={{ marginBottom: '16px' }} > - Disk Format + {t('mgmt.storage.diskformatTitle')} { + const { t } = useTranslation(); const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); const [mounts, setMounts] = useState([]); @@ -46,26 +48,26 @@ export const StorageMounts = () => { data={mounts} getKey={(r) => `${r.device} - ${refresh.path}`} buttons={[ - } variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>New Mount, + } variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>{t('mgmt.storage.newMount.newMountButton')}, } onClick={() => { refresh(); - }}>Refresh + }}>{t('global.refresh')} ]} columns={[ { - title: 'Device', + title: t('mgmt.storage.deviceTitle'), field: (r) => <> {r.device}, }, { - title: 'Path', + title: t('mgmt.storage.pathTitle'), field: (r) => r.path, }, { - title: 'Type', + title: t('mgmt.storage.typeTitle'), field: (r) => r.type, }, { - title: 'Options', + title: t('mgmt.storage.optionsTitle'), field: (r) => JSON.stringify(r.opts), }, { @@ -77,13 +79,13 @@ export const StorageMounts = () => { - Edit + {t('global.edit')} setMountDialog({data: r, unmount: true})}> - unmount + {t('global.unmount')}
diff --git a/client/src/pages/storage/parity.jsx b/client/src/pages/storage/parity.jsx index 5e72618e..b769418e 100644 --- a/client/src/pages/storage/parity.jsx +++ b/client/src/pages/storage/parity.jsx @@ -17,6 +17,7 @@ import SnapRAIDDialog, { SnapRAIDDialogInternal } from "./snapRaidDialog"; import MenuButton from "../../components/MenuButton"; import diskIcon from '../../assets/images/icons/disk.svg'; import ResponsiveButton from "../../components/responseiveButton"; +import { useTranslation } from 'react-i18next'; const getStatus = (status) => { if (!status) { @@ -56,6 +57,7 @@ const cleanStatus = (status) => { } export const Parity = () => { + const { t } = useTranslation(); const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); const [parities, setParities] = useState([]); @@ -118,7 +120,7 @@ export const Parity = () => { {(config) ? <> {deleteRaid && apiDeleteRaid(deleteRaid)} onClose={() => setDeleteRaid(null)} />} @@ -127,7 +129,7 @@ export const Parity = () => { } onClick={() => { refresh(); - }}>Refresh + }}>{t('global.refresh')}
{editOpened && } @@ -151,7 +153,7 @@ export const Parity = () => { }, }, { - title: 'Enabled', + title: t('global.enabled'), clickable:true, field: (r, k) => setEnabled(r.Name, !r.Enabled)} @@ -159,22 +161,22 @@ export const Parity = () => { />, }, { - title: 'Parity Disks', + title: t('mgmt.storage.parityDisksTitle'), field: (r) => r.Parity ? r.Parity.map(d =>
{d}
) : '-' }, { - title: 'Data Disks', + title: t('mgmt.storage.dataDisksTitle'), field: (r) => r.Parity ? Object.keys(r.Data).map(d =>
{d}: {r.Data[d]}
) : '-' }, { - title: 'Sync/Scrub Intervals', + title: t('mgmt.storage.syncScrubIntervalTitle'), screenMin: 'sm', - field: (r) =>
Sync: {crontabToText(r.SyncCrontab)}
Scrub: {crontabToText(r.ScrubCrontab)}
+ field: (r) =>
Sync: {crontabToText(r.SyncCrontab, t)}
Scrub: {crontabToText(r.ScrubCrontab, t)}
}, { - title: 'Status', + title: t('global.statusTitle'), screenMax: 'md', field: (r) => ({ error: , @@ -202,31 +204,31 @@ export const Parity = () => { - Edit + {t('global.edit')} sync(r.Name)}> - Sync + {t('mgmt.storage.list.syncText')} scrub(r.Name)}> - Scrub + {t('mgmt.storage.list.scrubText')} fix(r.Name)}> - Fix + {t('mgmt.storage.list.fixText')} tryDeleteRaid(r.Name)}> - Delete + {t('global.delete')}
diff --git a/client/src/pages/storage/smart.jsx b/client/src/pages/storage/smart.jsx index e391efee..9e61c07f 100644 --- a/client/src/pages/storage/smart.jsx +++ b/client/src/pages/storage/smart.jsx @@ -15,6 +15,7 @@ import { PascalToSnake, isDomain } from "../../utils/indexs"; import UploadButtons from "../../components/fileUpload"; import { useTheme } from '@mui/material/styles'; import MiniPlotComponent from '../dashboard/components/mini-plot'; +import { useTranslation } from 'react-i18next'; const temperatureChip = (temperature) => { if (temperature < 45) { @@ -190,6 +191,7 @@ const getSMARTDef = async() => { } const SMARTDialog = ({disk, OnClose}) => { + const { t } = useTranslation(); const fullData = disk.smart && disk.smart.AdditionalData && (disk.rota ? Object.values(disk.smart.AdditionalData.Attrs) : disk.smart.AdditionalData); @@ -197,16 +199,16 @@ const SMARTDialog = ({disk, OnClose}) => { return { OnClose && OnClose(); }}> - S.M.A.R.T. for {disk.name} + {t('mgmt.storage.smart.for')} {disk.name} - No S.M.A.R.T. data available for this disk. If you are running Cosmos behind some sort of virtualization or containerization, it is probably the reason why the data is not available. + {t('mgmt.storage.smart.noSmartError')} + }}>{t('global.close')} ; } @@ -214,17 +216,17 @@ const SMARTDialog = ({disk, OnClose}) => { return { OnClose && OnClose(); }}> - S.M.A.R.T. for {disk.name} + S.M.A.R.T. {t('mgmt.storage.smart.for')} {disk.name}
- health + {t('mgmt.storage.smart.health')} {healthChip(disk) + ' ' + healthStatus(disk, disk.rota ? fullData : []) + '%'}
- temperature + {t('global.temperature')} {(disk.smart && disk.smart.Temperature) ? `${temperatureChip(disk.smart.Temperature)} ${disk.smart.Temperature}°C` : '⚪ ?'}
@@ -256,11 +258,11 @@ const SMARTDialog = ({disk, OnClose}) => { getKey={(r) => `${r.Id}`} columns={[ { - title: 'Name', + title: t('global.nameTitle'), field: (r) => {r.def.display_name}, }, { - title: Value , + title: {t('mgmt.servapps.newContainer.env.envValueInput.envValueLabel')} , style: {minWidth: '110px'}, field: (r) => { let StatusIcon = ''; @@ -278,7 +280,7 @@ const SMARTDialog = ({disk, OnClose}) => { + }}>{t('global.close')}
}; diff --git a/client/src/pages/storage/snapRaidDialog.jsx b/client/src/pages/storage/snapRaidDialog.jsx index 2a8f6420..ecf84832 100644 --- a/client/src/pages/storage/snapRaidDialog.jsx +++ b/client/src/pages/storage/snapRaidDialog.jsx @@ -12,11 +12,13 @@ import { PlusCircleOutlined } from "@ant-design/icons"; import { crontabToText } from "../../utils/indexs"; import { MountPickerEntry } from "./mountPickerEntry"; import { json } from "react-router"; +import { Trans, useTranslation } from 'react-i18next'; -const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { +const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { + const { t } = useTranslation(); const formik = useFormik({ initialValues: { - name: data.Name || 'Storage Parity', + name: data.Name || t('mgmt.storage.snapraid.storageParity'), parity: data.Parity || [], data: data.Data || {}, syncCronTab: data.SyncCrontab || '0 0 2 * * *', @@ -24,9 +26,9 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { }, validateOnChange: false, validationSchema: yup.object({ - name: yup.string().required('Required').min(3, 'Name should be at least 3 characters').matches(/^[a-zA-Z0-9_ ]+$/, 'Name should be alphanumeric'), - parity: yup.array().min(1, 'Select at least 1 parity disk'), - data: yup.object().test('data', 'Select at least 2 data disk', (value) => { + name: yup.string().required(t('global.required')).min(3, t('mgmt.storage.snapraid.min3chars')).matches(/^[a-zA-Z0-9_ ]+$/, t('mgmt.storage.snapraid.notAlphanumeric')), + parity: yup.array().min(1, t('mgmt.storage.snapraid.min1parity')), + data: yup.object().test('data', t('mgmt.storage.snapraid.min2datadisks'), (value) => { return Object.keys(value).length >= 2; }) }), @@ -64,32 +66,28 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { - {data.Name ? ('Edit ' + data.Name) : 'Create Parity Disks'} + {data.Name ? (t('global.edit') + " "+ data.Name) : t('mgmt.storage.snapraid.createParityDisksButton')}
- You are about to create parity disks. This operation is safe and reversible. - Parity disks are used to protect your data from disk failure. - When creating a parity disk, the data disks you want to protect. Do not add a disk containing the system or another parity disk. +
- Step 1: First, select the parity disk(s). One parity disk will protect against one disk failure, two parity disks will protect against two disk failures, and so on. - Remember that those disks will be used only for parity, and will not be available for data storage. - Parity disks must be at least as large as the largest data disk, and should be empty. + {t('mgmt.storage.snapraid.createParity.step')} 1: {t('mgmt.storage.snapraid.createParity.Step1Text')} formik.setFieldValue('parity', value)} value={formik.values.parity} /> {formik.errors.parity && ( @@ -98,7 +96,7 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { )} - Step 2: Select the data disks you want to protect with the parity disk(s). + {t('mgmt.storage.snapraid.createParity.step')} 2: {t('mgmt.storage.snapraid.createParity.Step2Text')} @@ -123,12 +121,12 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { const data = formik.values.data; data[`disk${Object.keys(data).length}`] = ''; formik.setFieldValue('data', data); - }}>Add Data Disk + }}>{t('mgmt.storage.snapraid.addDatadisk')} + }}>{t('mgmt.storage.snapraid.removeDatadisk')}
{formik.errors.data && ( @@ -143,44 +141,44 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { )} - Step 3: Set the sync and scrub intervals. The sync interval is the time at which parity is updated. The scrub interval is the time at which the parity is checked for errors. This is using the CRONTAB syntax with seconds. + {t('mgmt.storage.snapraid.createParity.step')} 3: {t('mgmt.storage.snapraid.createParity.Step3Text')} - {crontabToText(formik.values.syncCronTab)} + {crontabToText(formik.values.syncCronTab, t)} - {crontabToText(formik.values.scrubCronTab)} + {crontabToText(formik.values.scrubCronTab, t)}
- + { formik.handleSubmit(); }}> - {data.Name ? 'Update' : 'Create'} + {data.Name ? t('global.update') : t('global.createAction')} @@ -190,6 +188,7 @@ const SnapRAIDDialogInternal = ({ refresh, open, setOpen, data = {}}) => { } const SnapRAIDDialog = ({ refresh, data }) => { + const { t } = useTranslation(); const [open, setOpen] = useState(false); return <> @@ -201,8 +200,8 @@ const SnapRAIDDialog = ({ refresh, data }) => { variant="contained" size="small" startIcon={} - >New Parity Disks : -
setOpen(true)}>Edit
} + >{t('mgmt.storage.snapraid.createParity.newDisks')} : +
setOpen(true)}>{t('global.edit')}
}
} diff --git a/client/src/utils/indexs.js b/client/src/utils/indexs.js index accae2c8..847ee955 100644 --- a/client/src/utils/indexs.js +++ b/client/src/utils/indexs.js @@ -51,11 +51,11 @@ export const redirectToLocal = (url) => { window.location.href = url; } -export const crontabToText = (crontab) => { +export const crontabToText = (crontab, t) => { const parts = crontab.split(' '); if (parts.length !== 6) { - return 'Invalid CRONTAB format (use 6 parts)'; + return t('mgmt.cron.invalidCron'); } const [second, minute, hour, dayOfMonth, month, dayOfWeek] = parts; diff --git a/client/src/utils/locales/de-CH/translation.json b/client/src/utils/locales/de-CH/translation.json new file mode 100644 index 00000000..cd9b3e33 --- /dev/null +++ b/client/src/utils/locales/de-CH/translation.json @@ -0,0 +1,11 @@ +{ + "global.close": "Schliessen", + "mgmt.config.security.geoblock.resetToDefaultButton": "Auf Standardwerte zurücksetzen (nur die erfahrungsgemäss risikoreichsten Länder)", + "mgmt.constellation.setup.dnsText": "Dies ist ein DNS, das innerhalb Ihres Constellation-Netzwerks läuft. Er schreibt die DNS-Einträge Ihrer Domänen automatisch so um, dass sie in Ihrem Netzwerk lokal sind, und ermöglicht es Ihnen ausserdem, Werbung und Tracker auf allen mit Ihrem Netzwerk verbundenen Geräten zu blockieren. Sie können auch benutzerdefinierte DNS-Einträge hinzufügen, um bestimmte IP-Adressen aufzulösen. Dieser DNS-Server ist nur von Ihrem Netzwerk aus zugänglich.", + "mgmt.constellation.setup.firewallInfo": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, Ports auf jedem Gerät einzeln zu öffnen und zu schliessen und festzulegen, wer auf sie zugreifen darf.", + "mgmt.constellation.setup.unsafeRoutesText": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, Ihren Datenverkehr über Ihre Geräte zu Dingen ausserhalb Ihrer Constellation zu tunneln.", + "mgmt.storage.snapraid.createParity.Step1Text": "Wählen Sie zunächst die Paritätsdisk aus. Eine Paritätsdisks schützt vor einem Datenträgerausfall, zwei Paritätsfestplatten vor zwei Datenträgerausfällen und so weiter. Denken Sie daran, dass diese Datenträger nur für die Parität verwendet werden und nicht für die Datenspeicherung zur Verfügung stehen. Die Paritätsdisks müssen mindestens so gross sein wie der grösste Datenträger und sollten leer sein.", + "mgmt.urls.edit.advancedSettings.filterIpWarning": "Mit dieser Einstellung werden alle Anfragen herausgefiltert, die nicht von den angegebenen IPs stammen. Dies erfordert, dass Ihr Setup die wahre IP des Clients meldet. Standardmässig wird dies der Fall sein, aber einige exotische Einstellungen (wie die Installation von docker/Cosmos unter Windows oder hinter Cloudlfare) verhindern, dass Cosmos die wahre IP des Clients kennt. Wenn Sie oben \"Restrict to Constellation\" verwendet haben, werden Constellation-IPs unabhängig von dieser Einstellung immer zugelassen.", + "navigation.monitoring.resourceDashboard.reasonBySmartShield": "Smart Shield (verschiedene Missbrauchsmetriken wie Zeit, Grösse, Brute-Force, gleichzeitige Anfragen, usw...). Es beinhaltet keine Sperrung für gesperrte IP, um Ressourcen im Falle von potenziellen Angriffen zu sparen.", + "navigation.monitoring.resourceDashboard.reasonByWhitelist": "Nach IP-Whitelists (einschliesslich der auf Constellation beschränkten)" +} \ No newline at end of file diff --git a/client/src/utils/locales/de/translation.json b/client/src/utils/locales/de/translation.json new file mode 100644 index 00000000..9745e0af --- /dev/null +++ b/client/src/utils/locales/de/translation.json @@ -0,0 +1,674 @@ +{ + "Storage": "Datenspeicher", + "auth.accountUnconfirmedError": "Sie haben Ihr Konto noch nicht registriert. Sie sollten einen Einladungslink in Ihren E-Mails haben. Wenn Sie eine neue Einladung benötigen, wenden Sie sich an Ihren Administrator.", + "auth.confirmPassword": "Passwort bestätigen", + "auth.enterPwd": "Bitte Passwort eingeben", + "auth.forgotPassword.backToLogin": "Zurück zum Login", + "auth.forgotPassword.checkEmail": "Überprüfen Sie Ihre E-Mail auf einen Link zum Zurücksetzen Ihres Passworts. Wenn er nicht innerhalb weniger Minuten erscheint, überprüfen Sie Ihren Spam-Ordner.", + "auth.forgotPassword.resetPassword": "Passwort zurücksetzen", + "auth.forgotPwd": "Passwort vergessen?", + "auth.genPwdStrength.good": "Gut", + "auth.genPwdStrength.normal": "Normal", + "auth.genPwdStrength.poor": "Zu schwach", + "auth.genPwdStrength.strong": "Stark", + "auth.genPwdStrength.weak": "Schwach", + "auth.hostnameInput": "Legen Sie zuerst Ihren Hostnamen fest", + "auth.loggedOutError": "Ihre Verbindung wurde unterbrochen. Bitte melden Sie sich an, um fortzufahren", + "auth.login": "Einloggen", + "auth.logoffText": "Sie sind abgemeldet worden. Wir leiten Sie weiter...", + "auth.notAdminError": "Sie müssen Administrator sein", + "auth.notLoggedInError": "Sie müssen eingeloggt sein, um darauf zuzugreifen", + "auth.pwd": "Passwort", + "auth.pwdRequired": "Passwort ist erforderlich", + "auth.pwdResetNotAllowed": "Auf diesem Server kann das Passwort nicht zurückgesetzt werden.", + "auth.selectOption": "Wählen Sie eine Option", + "auth.unexpectedErrorValidation": "Unerwarteter Fehler. Überprüfen Sie Ihre Daten oder versuchen Sie es später noch einmal.", + "auth.usernameInput": "Bitte Benutzername eigeben", + "auth.wrongCredError": "Falscher Benutzername oder falsches Passwort. Versuchen Sie es erneut oder setzen Sie Ihr Passwort zurück", + "auth.yourPassword": "Ihr Passwort", + "global.CPU": "CPU", + "global.RAM": "RAM", + "global.addAction": "Hinzufügen", + "global.backAction": "Zurück", + "global.cancelAction": "Abbrechen", + "global.close": "Schließen", + "global.confirmAction": "Bestätigen", + "global.confirmDeletion": "Sind Sie sicher?", + "global.copyFilenameSuffix": "Kopie", + "global.createAction": "Erstellen", + "global.createdAt": "Angelegt am", + "global.delete": "Löschen", + "global.description": "Beschreibung", + "global.driver": "Treiber", + "global.edit": "Bearbeiten", + "global.emailInvalidValidation": "Muss eine gültige E-Mail-Adresse sein", + "global.emailRequiredValidation": "E-Mail-Adresse ist erforderlich", + "global.enabled": "Aktiviert", + "global.error": "Fehler", + "global.hostname": "Hostname", + "global.logout": "Ausloggen", + "global.mount": "Verbinden", + "global.name.validation": "Name ist erforderlich", + "global.nameTitle": "Name", + "global.network": "Netzwerk", + "global.networks": "Netzwerke", + "global.never": "Nie", + "global.next": "Weiter", + "global.nicknameLabel": "Benutzername", + "global.nicknameRequiredValidation": "Benutzername ist erforderlich", + "global.refresh": "Aktualisieren", + "global.refreshPage": "Seite neu laden", + "global.required": "Erforderlich", + "global.resetZoomButton": "Zoom zurücksetzen", + "global.saveAction": "Speichern", + "global.savedConfirmation": "Gespeichert!", + "global.savedError": "Fehler beim Abspeichern, versuchen Sie es erneut.", + "global.searchPlaceholder": "Suche...", + "global.securityTitle": "Sicherheit", + "global.source": "Quelle", + "global.statusTitle": "Status", + "global.success": "Erfolgreich", + "global.target": "Ziel", + "global.temperature": "Temperatur", + "global.time": "Zeit", + "global.unmount": "Trennen", + "global.update": "Aktualisieren", + "global.user": "Benutzer", + "global.volume": "Volumen", + "header.notification.message.alertTriggered": "Die Warnmeldung \"{{Vars}}\" wurde ausgelöst.", + "header.notification.message.certificateRenewed": "Das TLS-Zertifikat für die folgenden Domains wurde erneuert: {{Vars}}", + "header.notification.message.containerUpdate": "Container {{Vars}} auf die neueste Version aktualisiert!", + "header.notification.title.alertTriggered": "Warnmeldung ausgelöst", + "header.notification.title.certificateRenewed": "Cosmos-Zertifikat erneuert", + "header.notification.title.containerUpdate": "App aktualisiert", + "header.notification.title.serverError": "Unbekannter Fehler", + "header.notificationTitle": "Benachrichtigungen", + "header.profileLabel": "Profil", + "header.settingLabel": "Einstellung", + "menu-items.management.configurationTitle": "Konfiguration", + "menu-items.management.constellation": "Constellation", + "menu-items.management.openId": "OpenID", + "menu-items.management.schedulerTitle": "Aufgabenplaner", + "menu-items.management.servApps": "ServApps", + "menu-items.management.storage": "Datenspeicher", + "menu-items.management.urls": "URLs", + "menu-items.management.usersTitle": "Benutzer", + "menu-items.managementTitle": "Verwaltung", + "menu-items.navigation": "Navigation", + "menu-items.navigation.home": "Home", + "menu-items.navigation.marketTitle": "Appstore", + "menu-items.navigation.monitoringTitle": "Metriken", + "menu-items.support": "Support", + "menu-items.support.bugReportTitle": "Fehler gefunden?", + "menu-items.support.discord": "Discord", + "menu-items.support.docsTitle": "Dokumentation", + "menu-items.support.github": "Github", + "mgmt.config.appearance.appDetailsOnHomepageCheckbox.appDetailsOnHomepageLabel": "Anwendungsdetails auf der Startseite anzeigen", + "mgmt.config.appearance.primaryColorSlider": "Primärfarbe", + "mgmt.config.appearance.resetColorsButton.resetColorsLabel": "Farben zurücksetzen", + "mgmt.config.appearance.resetWallpaperButton.resetWallpaperLabel": "Hintergrundbild zurücksetzen", + "mgmt.config.appearance.secondaryColorSlider": "Sekundärfarbe", + "mgmt.config.appearance.uploadWallpaperButton.previewBrokenError": "Die Vorschau scheint defekt zu sein. Bitte erneut hochladen.", + "mgmt.config.appearance.uploadWallpaperButton.uploadWallpaperLabel": "Hintergrundbild hochladen", + "mgmt.config.appearanceTitle": "Aussehen", + "mgmt.config.certRenewalLinktext": "dieser Link zur Dokumentation", + "mgmt.config.certRenewalText": "Sie verwenden Let's Encrypt, aber Sie verwenden nicht die DNS-Challenge mit einem Wildcard-Zertifikat. Das bedeutet, dass der Server das Zertifikat jedes Mal erneuern muss, wenn Sie einen neuen Hostnamen hinzufügen, was zu einigen Sekunden Ausfallzeit führt. Um dies in Zukunft zu vermeiden, lesen Sie bitte", + "mgmt.config.certRenewalTitle": "Erneuerung des Zertifikats", + "mgmt.config.containerPicker.containerNameSelection.containerNameLabel": "Containername", + "mgmt.config.containerPicker.containerNameSelection.containerNameValidation": "Bitte wählen Sie einen Container", + "mgmt.config.containerPicker.containerPortInput": "Containerport", + "mgmt.config.containerPicker.containerPortSelection.containerPortValidation": "Bitte geben Sie einen Port ein", + "mgmt.config.containerPicker.containerProtocolInput": "Container-Protokoll (verwenden Sie HTTP, wenn Sie unsicher sind, oder tcp für nicht-http Proxying)", + "mgmt.config.containerPicker.targetTypePreview": "Ergebnisvorschau", + "mgmt.config.containerPicker.targetTypePreview.targetTypePreviewLabel": "Wird automatisch generiert", + "mgmt.config.containerPicker.targetTypeValidation.noPort": "Ungültiges Ziel, muss einen Port haben", + "mgmt.config.containerPicker.targetTypeValidation.wrongProtocol": "Ungültiges Ziel, muss mit http:// oder https:// beginnen", + "mgmt.config.docker.defaultDatapathInput.defaultDatapathLabel": "Standard-Installationspfad", + "mgmt.config.docker.skipPruneImageCheckbox.skipPruneImageLabel": "Images nicht bereinigen", + "mgmt.config.docker.skipPruneNetworkCheckbox.skipPruneNetworkLabel": "Netzwerke nicht bereinigen", + "mgmt.config.email.enableCheckbox.enableHelperText": "SMTP aktivieren", + "mgmt.config.email.enableCheckbox.enableLabel": "SMTP aktivieren", + "mgmt.config.email.inbobox.label": "Damit können Sie einen SMTP-Server für Cosmos einrichten, um E-Mails wie Passwort-Reset-E-Mails und Einladungen zu versenden", + "mgmt.config.email.passwordInput.passwordHelperText": "SMTP-Kennwort", + "mgmt.config.email.passwordInput.passwordLabel": "SMTP-Kennwort", + "mgmt.config.email.selfSignedCheckbox.SelfSignedHelperText": "Selbstsigniertes Zertifikat zulassen", + "mgmt.config.email.selfSignedCheckbox.SelfSignedLabel": "Unsicheres TLS zulassen", + "mgmt.config.email.senderInput.senderHelperText": "SMTP-Absender", + "mgmt.config.email.senderInput.senderLabel": "SMTP-Absender", + "mgmt.config.email.tlsCheckbox.tlsLabel": "SMTP mit TLS", + "mgmt.config.email.usernameInput.usernameHelperText": "SMTP-Benutzername", + "mgmt.config.email.usernameInput.usernameLabel": "SMTP-Benutzername", + "mgmt.config.general.backupDirInput.backupDirHelperText": "Verzeichnis, in dem die Backups gespeichert werden (relativ zum Hostserver`/`)", + "mgmt.config.general.backupDirInput.backupDirLabel": "Verzeichnis für die Sicherung (relativ zum Hostserver`/`)", + "mgmt.config.general.configFileInfo": "Auf dieser Seite können Sie die Konfigurationsdatei bearbeiten. Alle Umgebungsvariablen, die die Konfiguration überschreiben, werden hier nicht angezeigt.", + "mgmt.config.general.forceMfaCheckbox.forceMfaHelperText": "MFA für alle Benutzer erforderlich machen", + "mgmt.config.general.forceMfaCheckbox.forceMfaLabel": "Erzwingen der Multi-Faktor-Authentifizierung", + "mgmt.config.general.logLevelInput": "Log-Stufe (Standard: INFO)", + "mgmt.config.general.logLevelInput.logLevelValidation": "Logging-Level ist erforderlich", + "mgmt.config.general.mongoDbInput": "MongoDB Verbindungs-String. Es wird empfohlen, stattdessen eine Umgebungsvariable zu verwenden, um diese sicher zu speichern. (Optional)", + "mgmt.config.general.monitoringCheckbox.monitoringLabel": "Überwachung Aktiviert", + "mgmt.config.general.notAdminWarning": "Da Sie kein Administrator sind, können Sie die Konfiguration nicht bearbeiten.
Diese Seite ist nur für die Sichtbarkeit da.", + "mgmt.config.general.puppetMode.configVolumeInput.configVolumeHelperText": "Konfigurationsverzeichnis Slave-Modus", + "mgmt.config.general.puppetMode.configVolumeInput.configVolumeLabel": "Konfigurationsverzeichnis Slave-Modus", + "mgmt.config.general.puppetMode.dbVolumeInput.dbVolumeHelperText": "Datenbankverzeichnis Slave-Modus", + "mgmt.config.general.puppetMode.dbVolumeInput.dbVolumeLabel": "Datenbankverzeichnis Slave-Modus", + "mgmt.config.general.puppetMode.enableCheckbox.enableHelperText": "Aktiviert den Slave-Modus", + "mgmt.config.general.puppetMode.enableCheckbox.enableLabel": "Slave-Modus Aktivieren", + "mgmt.config.general.puppetMode.hostnameInput.hostnameHelperText": "Hostname Slave-Modus", + "mgmt.config.general.puppetMode.hostnameInput.hostnameLabel": "Hostname Slave-Modus", + "mgmt.config.general.puppetMode.passwordInput.passwordHelperText": "Kennwort Slave-Modus", + "mgmt.config.general.puppetMode.passwordInput.passwordLabel": "Kennwort Slave-Modus", + "mgmt.config.general.puppetMode.usernameInput.usernameHelperText": "Benutzername Slave-Modus", + "mgmt.config.general.puppetMode.usernameInput.usernameLabel": "Benutzername Slave-Modus", + "mgmt.config.general.puppetMode.versionInput.versionHelperText": "Version Slave-Modus", + "mgmt.config.general.puppetMode.versionInput.versionLabel": "Version Slave-Modus", + "mgmt.config.general.puppetModeTitle": "Slave-Modus", + "mgmt.config.generalTitle": "Allgemein", + "mgmt.config.header.purgeMetricsButton.purgeMetricsLabel": "Metrik-Daten löschen", + "mgmt.config.header.purgeMetricsButton.purgeMetricsPopUp.cofirmAction": "Sind Sie sicher, dass Sie alle Metrikdaten aus den Dashboards löschen möchten?", + "mgmt.config.header.refreshButton.refreshLabel": "Aktualisieren", + "mgmt.config.header.restartButton.restartLabel": "Server Neustarten", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessLabel": "Unsicheren Zugriff über lokale IP zulassen", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessTooltip": "Wenn HTTPS zusammen mit einer Domain verwendet wird, ist es je nach Netzwerkkonfiguration möglich, dass Ihr Server keine direkten lokalen Verbindungen empfängt.
Mit dieser Option können Sie auch über Ihre lokale IP-Adresse auf Ihren Cosmos-Admin zugreifen, z. B. ip:port.
Sie können bereits ip:port-URLs für Ihre Anwendungen erstellen, aber dies macht sie zu HTTP-only.", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessWarning": "Diese Option wird nicht empfohlen, da sie Ihren Server den Sicherheitsrisiken in Ihrem lokalen Netzwerk aussetzt.
Ihr lokales Netzwerk ist sicherer als das Internet, aber nicht sicher, da Geräte wie IoT-Geräte, Smart-TVs, Smartphones oder sogar Ihr Router kompromittiert werden können.
Wenn Sie einen sicheren Offline-/Nur-Lokal-Zugang zu einem Server haben möchten, der einen Domänennamen und HTTPS verwendet, verwenden Sie stattdessen Constellation.", + "mgmt.config.http.allowSearchIndexCheckbox": "Aktivieren Sie diese Option, wenn Sie eine öffentliche Website haben und zulassen möchten, dass Suchmaschinen sie finden können, damit sie in den Suchergebnissen erscheint. ", + "mgmt.config.http.allowSearchIndexCheckbox.allowSearchIndexLabel": "Erlauben Sie Suchmaschinen, Ihren Server zu indizieren", + "mgmt.config.http.hostnameInput.HostnameLabel": "Hostname: Dies wird verwendet, um den Zugang zu Ihrem Cosmos-Server zu beschränken (Ihre IP oder Ihr Domainname)", + "mgmt.config.http.hostnameInput.HostnameValidation": "Hostname ist erforderlich", + "mgmt.config.proxy.noRoutesConfiguredText": "Keine Routen konfiguriert", + "mgmt.config.proxy.originTitle": "Ursprung", + "mgmt.config.proxy.refreshNeededWarning.notDomain": "Wenn Sie keinen Domänennamen verwenden, kann der Server für einige Sekunden offline gehen, um die Docker-Ports neu zuzuordnen.", + "mgmt.config.proxy.refreshNeededWarning.selfSigned": "Sie müssen die Seite aktualisieren, da Sie ein selbstsigniertes Zertifikat verwenden, falls Sie neue Zertifikate akzeptieren müssen. Um dies in Zukunft zu vermeiden, verwenden Sie bitte Let's Encrypt.", + "mgmt.config.proxy.saveChangesButton": "Änderungen Speichern", + "mgmt.config.proxy.urlTitle": "URL", + "mgmt.config.restart.laterButton": "Später", + "mgmt.config.restart.okButton": "OK", + "mgmt.config.restart.restartQuestion": "Möchten Sie Ihren Server neu starten?", + "mgmt.config.restart.restartStatus": "Server wird neu gestartet...", + "mgmt.config.restart.restartTimeoutWarning": "Der Neustart des Servers dauert länger als erwartet.", + "mgmt.config.restart.restartTimeoutWarningTip": "Ziehen Sie eine Fehlersuche in den Protokollen in Betracht. Wenn Sie ein selbstsigniertes Zertifikat verwenden, müssen Sie es möglicherweise aktualisieren und erneut akzeptieren.", + "mgmt.config.restart.restartTitle": "Server Neustarten?", + "mgmt.config.security.adminRestrictions.adminConstellationCheckbox.adminConstellationLabel": "Zugriff auf das Admin-Panel nur von Constellation aus erlauben", + "mgmt.config.security.adminRestrictions.adminRestrictionsInfo": "Aktivieren Sie diese Option, um den Zugriff auf das Admin-Panel zu beschränken. Seien Sie vorsichtig, wenn Sie sich selbst aussperren, müssen Sie die Konfigurationsdatei manuell bearbeiten. Um den Zugriff auf Ihr lokales Netzwerk zu beschränken, können Sie die \"Admin Whitelist\" mit dem IP-Bereich 192.168.0.0/16 verwenden ", + "mgmt.config.security.adminRestrictions.adminWhitelistInput.adminWhitelistHelperText": "Durch Komma getrennte Liste der IPs, die auf das Admin-Panel zugreifen dürfen", + "mgmt.config.security.adminRestrictions.adminWhitelistInput.adminWhitelistLabel": "Admin-Whitelist für eingehende IPs und/oder IP-Bereiche (durch Komma getrennt)", + "mgmt.config.security.adminRestrictionsTitle": "Administrative Einschränkungen", + "mgmt.config.security.encryption.authPubKeyTitle": "Öffentlicher Schlüssel zur Authentifizierung", + "mgmt.config.security.encryption.enryptionInfo": "Aus Sicherheitsgründen ist es nicht möglich, die privaten Schlüssel von Zertifikaten auf Ihrer Instanz aus der Ferne zu ändern. Es wird empfohlen, die Konfigurationsdatei manuell zu bearbeiten oder besser, Umgebungsvariablen zu verwenden, um sie zu speichern.", + "mgmt.config.security.encryption.genMissingAuthCheckbox.genMissingAuthLabel": "Fehlende Authentifizierungszertifikate automatisch generieren (Standard: Ja)", + "mgmt.config.security.encryption.httpsCertSelection.httpsCertLabel": "HTTPS-Zertifikate", + "mgmt.config.security.encryption.httpsCertSelection.sslDisabledChoice": "Verwenden Sie kein HTTPS (sehr unsicher)", + "mgmt.config.security.encryption.httpsCertSelection.sslLetsEncryptChoice": "Automatisches Erstellen von Zertifikaten mit Let's Encrypt (empfohlen)", + "mgmt.config.security.encryption.httpsCertSelection.sslProvidedChoice": "Ich habe meine eigenen Zertifikate", + "mgmt.config.security.encryption.httpsCertSelection.sslSelfSignedChoice": "Lokale, selbstsignierte Zertifikate (unsicher)", + "mgmt.config.security.encryption.overwriteWildcardInput.overwriteWildcardLabel": "(optional, nur für fortgeschrittene Nutzer) Überschreibe Wildcard Domains (Syntax wie im Platzhalter, durch komma getrennt)", + "mgmt.config.security.encryption.rootHttpsPubKeyTitle": "Öffentlicher HTTPS-Root-Schlüssel", + "mgmt.config.security.encryption.sslCertForceRenewCheckbox.sslCertForceRenewLabel": "Erneuerung des HTTPS-Zertifikats beim nächsten Speichern erzwingen", + "mgmt.config.security.encryption.sslLetsEncryptDnsSelection.sslLetsEncryptDnsLabel": "DNS-Anbieter (nur bei DNS-Challenge, sonst leer lassen)", + "mgmt.config.security.encryption.sslLetsEncryptEmailInput.sslLetsEncryptEmailLabel": "E-Mail Adresse für Let's Encrypt", + "mgmt.config.security.encryption.wildcardCheckbox.wildcardLabel": "Verwenden Sie ein Wildcard-Zertifikat für die Stammdomäne von ", + "mgmt.config.security.encryptionTitle": "Verschlüsselung", + "mgmt.config.security.geoBlockSelection": "Wählen Sie die Länder, die Sie {{blockAllow}} möchten", + "mgmt.config.security.geoBlockSelection.geoBlockLabel": "Geo-Blocking: (Diese Länder sind auf Ihrem Server {{blockAllow}})", + "mgmt.config.security.geoBlockSelection.geoBlockLabel.varAllow": "für den Zugriff freigegeben", + "mgmt.config.security.geoBlockSelection.geoBlockLabel.varBlock": "für den Zugriff gesperrt", + "mgmt.config.security.geoBlockSelection.varAllow": "freigeben", + "mgmt.config.security.geoBlockSelection.varBlock": "sperren", + "mgmt.config.security.geoblock.resetToDefaultButton": "Auf Standardwerte zurücksetzen (nur die erfahrungsgemäß risikoreichsten Länder)", + "mgmt.config.security.invertBlacklistCheckbox.invertBlacklistLabel": "Liste als Whitelist anstelle einer Blacklist verwenden", + "mgmt.constellation.dns.resetButton": "Zurücksetzen", + "mgmt.constellation.dnsBlocklistsTitle": "DNS Blocklisten", + "mgmt.constellation.dnsTitle": "Constellation-Internes DNS", + "mgmt.constellation.externalText": "Sie sind derzeit mit einem externen Constellation-Netzwerk verbunden. Verwenden Sie Ihren Haupt-Cosmos-Server, um Ihr Constellation-Netzwerk und Ihre Geräte zu verwalten.", + "mgmt.constellation.isRelay.label": "Kann Datenverkehr weiterleiten", + "mgmt.constellation.resetLabel": "Netzwerk zurücksetzen", + "mgmt.constellation.resetText": "Dadurch wird das Netzwerk vollständig zurückgesetzt und die Verbindung zu allen Clients unterbrochen. Sie müssen sie dann wieder verbinden. Dies kann nicht rückgängig gemacht werden.", + "mgmt.constellation.restartButton": "VPN-Service neustarten", + "mgmt.constellation.setup.addDeviceSuccess": "Gerät erfolgreich hinzugefügt! Laden Sie den QR-Code aus der Cosmos-App herunter oder laden Sie die relevanten Dateien zusammen mit der Konfiguration und dem Netzwerkzertifikat auf Ihr Gerät herunter, um eine Verbindung herzustellen:", + "mgmt.constellation.setup.addDeviceText": "Fügen Sie ein Gerät zur Konstellation hinzu, indem Sie entweder den Cosmos- oder den Nebula-Client verwenden", + "mgmt.constellation.setup.addDeviceTitle": "Gerät hinzufügen", + "mgmt.constellation.setup.deviceName.label": "Gerätenamen", + "mgmt.constellation.setup.dns.customEntries": "Manuelle DNS-Einträge", + "mgmt.constellation.setup.dns.resetDefault": "Auf Standard zurücksetzen", + "mgmt.constellation.setup.dnsBlocklistText": "Blacklisten zum Blockieren von Domains verwenden", + "mgmt.constellation.setup.dnsBlocklistUrls.label": "DNS-Blocklisten-URLs", + "mgmt.constellation.setup.dnsExpiryWarning": "Wenn Sie Ihre DNS-Einträge ändern, sollten Sie immer den privaten Modus Ihres Browsers verwenden und einige Zeit verstreichen lassen, bis die verschiedenen Caches abgelaufen sind.", + "mgmt.constellation.setup.dnsText": "Dies ist ein DNS, das innerhalb Ihres Constellation-Netzwerks läuft. Er schreibt die DNS-Einträge Ihrer Domänen automatisch so um, dass sie in Ihrem Netzwerk lokal sind, und ermöglicht es Ihnen außerdem, Werbung und Tracker auf allen mit Ihrem Netzwerk verbundenen Geräten zu blockieren. Sie können auch benutzerdefinierte DNS-Einträge hinzufügen, um bestimmte IP-Adressen aufzulösen. Dieser DNS-Server ist nur von Ihrem Netzwerk aus zugänglich.", + "mgmt.constellation.setup.enabledCheckbox": "Constellation aktiviert", + "mgmt.constellation.setup.externalConfig.label": "Externe Constellation-Netzwerk-Datei hochladen", + "mgmt.constellation.setup.firewallInfo": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, Ports auf jedem Gerät einzeln zu öffnen und zu schließen und festzulegen, wer auf sie zugreifen darf.", + "mgmt.constellation.setup.hostnameInfo": "Dies ist Ihr Constellation-Hostname, den Sie für die Verbindung verwenden werden. Wenn Sie einen Domänennamen verwenden, muss sich dieser vom Hostnamen Ihres Servers unterscheiden. Unabhängig von der von Ihnen gewählten Domain ist es sehr wichtig, dass Sie sicherstellen, dass ein A-Eintrag in Ihrem Domain-DNS auf diesen Server verweist. Wenn Sie diesen Wert ändern, müssen Sie Ihr Netzwerk zurücksetzen und alle Clients neu verbinden!", + "mgmt.constellation.setup.ip.label": "Constellation IP-Addresse", + "mgmt.constellation.setup.ipTitle": "Constellation-IP", + "mgmt.constellation.setup.owner.label": "Besitzer", + "mgmt.constellation.setup.privNode.label": "Dieser Knoten ist privat (keine öffentliche IP)", + "mgmt.constellation.setup.pubHostname.label": "Öffentlicher Hostname", + "mgmt.constellation.setup.pubKey.label": "Öffentlicher Schlüssel (optional)", + "mgmt.constellation.setup.relayRequests.label": "Anfragen über diesen Knoten weiterleiten", + "mgmt.constellation.setup.unsafeRoutesText": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, Ihren Datenverkehr über Ihre Geräte zu Dingen außerhalb Ihrer Constellation zu tunneln.", + "mgmt.constellation.setup.unsafeRoutesTitle": "Unsichere Routen", + "mgmt.constellation.setupText": "Constellation ist ein VPN, das innerhalb Ihres Cosmos-Netzwerks läuft. Es verbindet automatisch alle Ihre Geräte miteinander und ermöglicht Ihnen den Zugriff von überall. Bitte lesen Sie die <0>Dokumentation für weitere Informationen. Um eine Verbindung herzustellen, verwenden Sie bitte die <1>Constellation App. Constellation ist derzeit bis zum Ende der Beta-Phase, die für Januar 2024 geplant ist, kostenlos nutzbar.", + "mgmt.constellation.setupTitle": "Constellation einrichten", + "mgmt.constellation.setuplighthouseTitle": "Lighthouse Einrichtung", + "mgmt.constellation.showConfigButton": "VPN-Konfiguration anzeigen", + "mgmt.constellation.showLogsButton": "VPN-Protokolle anzeigen", + "mgmt.cron.editCron.customText": "Erstellen Sie einen benutzerdefinierten Auftrag zur Ausführung eines Shell-Befehls in einem Container. Lassen Sie das Feld \"Container\" leer, um den Auftrag auf dem Host auszuführen", + "mgmt.cron.editCron.customText.onHostOnly": "Die Ausführung auf dem Host funktioniert nur, wenn Cosmos selbst nicht in einem Container ausgeführt wird.", + "mgmt.cron.editCronTitle": "Aufgabe bearbeiten", + "mgmt.cron.invalidCron": "Ungültiges CRONTAB-Format (6 Abschnitte verwenden)", + "mgmt.cron.list.state.lastRan": "zuletzt ausgeführt", + "mgmt.cron.list.state.running": "Läuft - gestartet", + "mgmt.cron.newCron.commandInput.commandLabel": "Auszuführender Befehl (z. B. echo 'Hallo Welt')", + "mgmt.cron.newCron.cronNameInput.cronNameLabel": "Name der Aufgabe", + "mgmt.cron.newCron.crontabInput.crontabLabel": "Zeitplan (unter Verwendung der crontab-Syntax)", + "mgmt.cron.newCron.submitButton": "Absenden", + "mgmt.cron.newCronTitle": "Neue Aufgabe", + "mgmt.monitoring.alerts.actionTriggersTitle": "Auslöser", + "mgmt.monitoring.alerts.addActionButton": "Aktion hinzufügen", + "mgmt.openId.experimentalWarning": "Dies ist eine experimentelle Funktion. Es wird empfohlen, sie mit Vorsicht zu verwenden. Bitte melden Sie jedes Problem, das Sie finden!", + "mgmt.openId.newSecret": "Neues Secret", + "mgmt.openId.redirect": "Weiterleitung", + "mgmt.openId.redirectUri": "Weiterleitungsziel", + "mgmt.openId.resetSecret": "Schlüssel zurücksetzen", + "mgmt.openId.secretUpdated": "Das Secret wurde aktualisiert. Bitte kopieren Sie es jetzt, da es nicht mehr angezeigt werden wird.", + "mgmt.openid.newClientTitle": "Neuer Client", + "mgmt.openid.newMfa": "Neue MFA einrichten", + "mgmt.openid.newMfa.enterOtp": "Geben Sie Ihr OTP ein", + "mgmt.openid.newMfa.otpEnterTokenText": "Nachdem Sie den QR-Code gescannt oder den Code manuell eingegeben haben, geben Sie das Token aus Ihrer Authentifizierungs-App unten ein", + "mgmt.openid.newMfa.otpManualCode": "...Oder geben Sie diesen Code manuell ein", + "mgmt.openid.newMfa.otpManualCode.showButton": "Manuellen Code anzeigen", + "mgmt.openid.newMfa.requires2faText": "Dieser Server erfordert 2FA. Scannen Sie diesen QR-Code mit Ihrer <0 title=\"Zum Beispiel FreeOTP(+) oder Google/Microsoft-Authentifikator\"><1>Authenticator-App, um fortzufahren", + "mgmt.openid.newMfa.tokenRequiredValidation": "Token ist erforderlich", + "mgmt.openid.newMfa.tokenmax6charValidation": "Token darf maximal 6 Zeichen lang sein", + "mgmt.openid.newMfa.tokenmin6charValidation": "Token muss mindestens 6 Zeichen lang sein", + "mgmt.openid.newMfa.wrongOtpValidation": "Falsches OTP. Bitte erneut versuchen", + "mgmt.scheduler.customJobsTitle": "Benutzerdefinierte Aufgaben", + "mgmt.scheduler.lastLogs": "Letzes Protokoll für", + "mgmt.scheduler.list.action.logs": "Protokolle", + "mgmt.scheduler.list.action.run": "Ausführen", + "mgmt.scheduler.list.scheduleTitle": "Zeitplan", + "mgmt.scheduler.list.status.lastRunExitedOn": "Der letzte Lauf endete mit einem Fehler bei", + "mgmt.scheduler.list.status.lastRunFinishedOn": "Letzter Lauf beendet um", + "mgmt.scheduler.list.status.lastRunFinishedOn.duration": "Dauer", + "mgmt.scheduler.list.status.neverRan": "Nie ausgeführt", + "mgmt.scheduler.list.status.runningSince": "Läuft seit ", + "mgmt.scheduler.oneTimeJobsTitle": "Einmalige Aufgaben", + "mgmt.scheduler.parityDiskJobsTitle": "Aufgaben für Paritätsfestplatten", + "mgmt.servApp.container.urls.exposeText": "Willkommen beim URL-Assistenten. Diese Schnittstelle hilft Ihnen, Ihre ServApp sicher dem Internet auszusetzen, indem Sie eine neue URL erstellen.", + "mgmt.servApp.container.urls.exposeTitle": "ServApp freigeben", + "mgmt.servApp.exposeDesc": "containerName im Internet veröffentlichen", + "mgmt.servApp.newContainer.reviewStartButton": "Überprüfen & starten", + "mgmt.servApp.newServAppButton": "Neues ServApp starten", + "mgmt.servApp.url": "Erstellen Sie eine URL für den Zugriff auf diese ServApp", + "mgmt.servApps.autoUpdateCheckbox": "Container automatisch aktualisieren", + "mgmt.servApps.container.delete.cronjob": "Aufgabe", + "mgmt.servApps.container.delete.done": "Fertig", + "mgmt.servApps.container.delete.route": "Route", + "mgmt.servApps.container.deleteService": "Service löschen", + "mgmt.servApps.container.deleteServiceStatus": "Löschungsstatus:", + "mgmt.servApps.container.network.linkContainerButton": "Container verknüpfen", + "mgmt.servApps.container.network.linkContainerTitle": "mit Container verknüpfen", + "mgmt.servApps.container.overview.healthTitle": "Zustand", + "mgmt.servApps.container.overview.imageTitle": "Image", + "mgmt.servApps.container.overview.ipAddressTitle": "IP-Adresse", + "mgmt.servApps.container.overview.settingsTitle": "Einstellungen", + "mgmt.servApps.container.protocols.errorOnlyCheckbox": "Nur Fehler", + "mgmt.servApps.container.selectWhatToDelete": "Wählen Sie aus, was Sie löschen möchten:", + "mgmt.servApps.createNetwork.parentReqForMacvlan": "Die übergeordnete Schnittstelle ist für MACVLAN erforderlich.", + "mgmt.servApps.createdChip.createdLabel": "Erstellt", + "mgmt.servApps.deadChip.deadLabel": "Tot", + "mgmt.servApps.driver.none": "Kein", + "mgmt.servApps.exitedChip.exitedLabel": "Beendet", + "mgmt.servApps.exportDockerBackupButton.exportDockerBackupLabel": "Docker-Backup exportieren", + "mgmt.servApps.networks.containerPortInput.containerPortLabel": "Container-Port", + "mgmt.servApps.networks.containerotRunningWarning": "Dieser Container läuft nicht. Wenn Sie die Einstellungen ändern, wird der Container wieder gestartet.", + "mgmt.servApps.networks.exposePortsTitle": "Ports freigeben", + "mgmt.servApps.networks.forcedSecurityWarning": "Dieser Container muss gesichert werden. Sie können keine Ports direkt zum Internet freigeben, bitte erstellen Sie stattdessen eine URL in Cosmos. Sie können ihn auch nicht mit dem Bridge-Netzwerk verbinden.", + "mgmt.servApps.networks.modeInput.modeLabel": "Netzwerkmodus", + "mgmt.servApps.networks.removedNetConnectedDisconnect": "Trennen Sie die Verbindung", + "mgmt.servApps.networks.removedNetConnectedEitherRecreate": "Entweder erstellen Sie es neu oder", + "mgmt.servApps.networks.removedNetConnectedError": "Sie sind mit einem Netzwerk verbunden, das entfernt worden ist:", + "mgmt.servApps.networks.updatePortsButton": "Ports aktualisieren", + "mgmt.servApps.newChip.newLabel": "Neu", + "mgmt.servApps.newContainer.chooseUrl": "Wählen Sie eine URL für", + "mgmt.servApps.newContainer.cosmosOutdatedError": "Dieser Dienst erfordert eine neuere Version von Cosmos. Bitte aktualisieren Sie Cosmos, um diesen Dienst zu installieren.", + "mgmt.servApps.newContainer.customize": " ", + "mgmt.servApps.newContainer.customize2": " anpassen", + "mgmt.servApps.newContainer.networkSettingsTitle": "Netzwerkeinstellungen", + "mgmt.servApps.newContainer.serviceNameInput": "Wählen Sie Ihren Servicenamen", + "mgmt.servApps.notRunningWarning": "Dieser Container ist nicht in Betrieb. Wenn Sie die Einstellungen ändern, wird der Container wieder gestartet.", + "mgmt.servApps.pausedChip.pausedLabel": "Pausiert", + "mgmt.servApps.removingChip.removingLabel": "Beenden", + "mgmt.servApps.restartingChip.restartingLabel": "Neustarten", + "mgmt.servApps.runningChip.runningLabel": "Läuft", + "mgmt.servApps.startToEditInfo": "Container zum Bearbeiten starten", + "mgmt.servApps.volumes.containerNotRunningWarning": "Dieser Container ist nicht in Betrieb. Wenn Sie die Einstellungen ändern, wird der Container wieder gestartet.", + "mgmt.servApps.volumes.newVolume.driverSelection.localChoice": "Lokal", + "mgmt.servApps.volumes.newVolumeTitle": "Neues Volumen", + "mgmt.servapps.actionBar.kill": "Stopp Erzwingen", + "mgmt.servapps.actionBar.noUpdate": "Keine neue Version verfügbar. Klicken, um Neuinstallation zu erzwingen", + "mgmt.servapps.actionBar.pause": "Pausieren", + "mgmt.servapps.actionBar.recreate": "Neu erstellen", + "mgmt.servapps.actionBar.restart": "Neustarten", + "mgmt.servapps.actionBar.start": "Starten", + "mgmt.servapps.actionBar.stop": "Stoppen", + "mgmt.servapps.actionBar.unpause": "Fortsetzen", + "mgmt.servapps.actionBar.update": "Aktualisierung Verfügbar", + "mgmt.servapps.actionBar.updating": "ServApp wird aktualisiert...", + "mgmt.servapps.compose": "Compose", + "mgmt.servapps.compose.installButton": "Installieren", + "mgmt.servapps.compose.installTitle": "Installation", + "mgmt.servapps.container.compose.createServiceButton": "Service erstellen - Vorschau", + "mgmt.servapps.container.compose.createServiceSuccess": "Service erstellt!", + "mgmt.servapps.container.compose.editServiceTitle": "Service bearbeiten", + "mgmt.servapps.containers.terminal.connectButton": "Verbinden", + "mgmt.servapps.containers.terminal.connectedToText": "Verbunden mit ", + "mgmt.servapps.containers.terminal.disconnectButton": "Trennen", + "mgmt.servapps.containers.terminal.disconnectedFromText": "Verbindung getrennt von ", + "mgmt.servapps.containers.terminal.mainprocessTty": "TTY Hauptprozess", + "mgmt.servapps.containers.terminal.newShellButton": "Neues Fenster", + "mgmt.servapps.containers.terminal.terminalNotInteractiveWarning": "Dieser Container ist nicht interaktiv. Falls Sie sich mit dem Hauptprozess verbinden wollen:", + "mgmt.servapps.containers.terminal.ttyEnableButton": "TTY einschalten", + "mgmt.servapps.importComposeFileButton": "Importiere Compose-Datei", + "mgmt.servapps.networks.attackNetwork": "an Cosmos Anhängen ", + "mgmt.servapps.networks.containers": "Container", + "mgmt.servapps.networks.list.bridge": "Bridge", + "mgmt.servapps.networks.list.host": "Host", + "mgmt.servapps.networks.list.macvlan": "MACVLAN", + "mgmt.servapps.networks.list.networkIpam": "IPAM-Gateway / Maske", + "mgmt.servapps.networks.list.networkName": "Netzwerkname", + "mgmt.servapps.networks.list.networkNoIp": "Keine IP", + "mgmt.servapps.networks.list.networkproperties": "Eigenschaften", + "mgmt.servapps.networks.list.newNetwork": "Neues Netzwerk", + "mgmt.servapps.networks.list.overlay": "Overlay", + "mgmt.servapps.networks.list.parentIf": "Übergeordnete Schnittstelle", + "mgmt.servapps.networks.list.subnet": "Subnetz (Optional)", + "mgmt.servapps.networks.volumes": "Volumen", + "mgmt.servapps.newContainer.devices.containerPathInput.containerPathLabel": "Dateipfad Container", + "mgmt.servapps.newContainer.devices.hostPathInput.hostPathLabel": "Dateipfad Host", + "mgmt.servapps.newContainer.devicesTitle": "Geräte", + "mgmt.servapps.newContainer.env.envKeyInput.envKeyLabel": "Schlüssel", + "mgmt.servapps.newContainer.env.envValueInput.envValueLabel": "Wert", + "mgmt.servapps.newContainer.env.keyNotUniqueError": "Umgebungsvariablen müssen eindeutig sein", + "mgmt.servapps.newContainer.envTitle": "Umgebungsvariablen", + "mgmt.servapps.newContainer.forceSecureCheckbox.forceSecureLabel": "sicheres Containernetzwerk erzwingen", + "mgmt.servapps.newContainer.imageUpdateWarning": "Sie haben das Image aktualisiert. Ein Klick auf die Schaltfläche unten wird das neue Image herunterladen, und erst dann können Sie den Container aktualisieren.", + "mgmt.servapps.newContainer.interactiveCheckbox.interactiveLabel": "Interaktiver Modus", + "mgmt.servapps.newContainer.label.labelNotUniqueError": "Label muss eindeutig sein", + "mgmt.servapps.newContainer.labelsTitle": "Labels", + "mgmt.servapps.newContainer.pullImageButton": "Neues Image Herunterladen", + "mgmt.servapps.newContainer.pullingImageStatus": "Neues Image abrufen...", + "mgmt.servapps.newContainer.restartPolicyInput.restartPolicyLabel": "Neustart-Policy", + "mgmt.servapps.newContainer.restartPolicyInput.restartPolicyPlaceholder": "Neustart-Policy", + "mgmt.servapps.newContainer.updateContainerButton": "Container Aktualisieren", + "mgmt.servapps.newContainer.volumes.bindInput": "Mapping", + "mgmt.servapps.newContainer.volumes.mountNotUniqueError": "Mappings müssen eindeutige Ziele haben", + "mgmt.servapps.newContainer.volumes.newMountButton": "Neues Mapping", + "mgmt.servapps.newContainer.volumes.updateVolumesButton": "Volumen Aktualisieren", + "mgmt.servapps.newContainer.volumesTitle": "Volumen-Mappings", + "mgmt.servapps.newContainerTitle": "Einrichtung Docker Container", + "mgmt.servapps.overview": "Übersicht", + "mgmt.servapps.pasteComposeButton.pasteComposePlaceholder": "Fügen Sie Ihre docker-compose.yml / cosmos-compose.json hier ein oder verwenden Sie die Schaltfläche zum Hochladen der Datei.", + "mgmt.servapps.routeConfig.routeNotFound": "Keine Route gefunden", + "mgmt.servapps.routeConfig.setup": "Konfiguration", + "mgmt.servapps.terminal": "Konsole", + "mgmt.servapps.updatesAvailableFor": "Update sind verfügbar für", + "mgmt.servapps.viewDetailsButton": "Details ansehen", + "mgmt.servapps.viewStackButton": "Stack ansehen", + "mgmt.servapps.volumes.list.ScopeTitle": "Umfang", + "mgmt.servapps.volumes.volumeName": "Volumenname", + "mgmt.storage.available": "verfügbar", + "mgmt.storage.chown": "Eigentümer des Einhängeordners ändern (optional, z. B. 1000:1000)", + "mgmt.storage.configName.configNameLabel": "Konfigurationsname", + "mgmt.storage.confirmParityDeletion": "Sind Sie sicher, dass Sie diese Parität löschen wollen?", + "mgmt.storage.confirmPwd.confirmPwdLabel": "Bestätigen Sie Ihr Passwort", + "mgmt.storage.dataDisksTitle": "Datenträger", + "mgmt.storage.deviceTitle": "Gerät", + "mgmt.storage.diskformatTitle": "Datenträgerformat", + "mgmt.storage.disks": "Datenträger", + "mgmt.storage.externalStorage": "Externe Datenträger", + "mgmt.storage.externalStorageText": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, externe Clouds (Dropbox, Onedrive, ...) auf Ihren Server zu mounten.", + "mgmt.storage.formatButton": "Formatieren", + "mgmt.storage.formatDiskTitle": "Datenträger formatieren", + "mgmt.storage.formattingLog": "wird formatiert", + "mgmt.storage.list.fixText": "Fehler korrigieren", + "mgmt.storage.list.scrubText": "Scrub", + "mgmt.storage.list.syncText": "Synchronisieren", + "mgmt.storage.merge.fsOptions.fsOptionsLabel": "Zusätzliche mergerFS-Optionen (optional, durch Komma getrennt)", + "mgmt.storage.mergeButton": "Zusammenführen", + "mgmt.storage.mergeText": "Sie sind dabei, Datenträger zusammenzuführen. Dieser Vorgang ist sicher und umkehrbar. Er wirkt sich nicht auf die Daten auf den Datenträgern aus, sondern stellt den Inhalt im Datei-Explorer als einen einzigen Datenträger zur Verfügung.", + "mgmt.storage.mergeTitle": "Datenträger Zusammenführen", + "mgmt.storage.mount.permanent": "Permanent", + "mgmt.storage.mount.whatToMountLabel": "Was wollen Sie einbinden", + "mgmt.storage.mountPath": "Pfad zum Einbinden", + "mgmt.storage.mountPicker": "Disks auswählen", + "mgmt.storage.mountedAtText": "verbunden mit", + "mgmt.storage.mounts": "Einbindungen", + "mgmt.storage.newMerge.newMergeButton": "Neue Verknüpfung", + "mgmt.storage.newMount.newMountButton": "Neue Einbindung", + "mgmt.storage.optionsTitle": "Optionen", + "mgmt.storage.parityDisksTitle": "Paritätsdisk", + "mgmt.storage.parityTitle": "Parität", + "mgmt.storage.pathPrefixMntValidation": "Der Pfad sollte mit /mnt/ oder /var/mnt beginnen", + "mgmt.storage.pathTitle": "Dateipfad", + "mgmt.storage.raidText": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, RAID-Arrays mit Ihren Datenträgern zu erstellen.", + "mgmt.storage.raidTitle": "RAID", + "mgmt.storage.runningInsideContainerWarning": "Sie führen Cosmos innerhalb eines Docker-Containers aus. Als solcher hat er nur begrenzten Zugriff auf Ihre Festplatten und deren Informationen.", + "mgmt.storage.selectMin2": "Wählen Sie mindestens 2 Datenträger", + "mgmt.storage.sharesText": "Demnächst verfügbar. Diese Funktion wird es Ihnen ermöglichen, Ordner mit verschiedenen Protokollen (SMB, FTP, ...) zu teilen.", + "mgmt.storage.sharesTitle": "Freigaben", + "mgmt.storage.smart.for": "S.M.A.R.T. für", + "mgmt.storage.smart.health": "Zustand", + "mgmt.storage.smart.noSmartError": "Für diesen Datenträger sind keine S.M.A.R.T.-Daten verfügbar. Wenn Sie Cosmos hinter einer Art von Virtualisierung oder Containerisierung ausführen, ist dies wahrscheinlich der Grund, warum die Daten nicht verfügbar sind.", + "mgmt.storage.smart.threshholdTooltip": "Dieser Wert ist ein Prozentsatz der Gesundheit (100 ist am besten). Daneben gibt es einen Schwellenwert, bei dessen Unterschreitung die Festplatte dringend ersetzt werden sollte.", + "mgmt.storage.snapraid.addDatadisk": "Datenträger hinzufügen", + "mgmt.storage.snapraid.createParity.Step1Text": "Wählen Sie zunächst die Paritätsdisk aus. Eine Paritätsdisks schützt vor einem Datenträgerausfall, zwei Paritätsfestplatten vor zwei Datenträgerausfällen und so weiter. Denken Sie daran, dass diese Datenträger nur für die Parität verwendet werden und nicht für die Datenspeicherung zur Verfügung stehen. Die Paritätsdisks müssen mindestens so groß sein wie der größte Datenträger und sollten leer sein.", + "mgmt.storage.snapraid.createParity.Step2Text": "Wählen Sie die Datenplatten aus, die Sie mit der/den Paritätsplatte(n) schützen wollen.", + "mgmt.storage.snapraid.createParity.Step3Text": "Legen Sie die Sync- und Scrub-Intervalle fest. Das Sync-Intervall ist der Zeitpunkt, zu dem die Parität aktualisiert wird. Das Scrub-Intervall ist der Zeitpunkt, zu dem die Parität auf Fehler geprüft wird. Hier wird die CRONTAB-Syntax mit Sekunden verwendet.", + "mgmt.storage.snapraid.createParity.newDisks": "Neue Paritäts-Disks", + "mgmt.storage.snapraid.createParity.step": "Schritt", + "mgmt.storage.snapraid.createParityDisksButton": "Paritätsdisks erstellen", + "mgmt.storage.snapraid.createParityInfo": "Sie sind dabei, Paritätsdisk zu erstellen. Dieser Vorgang ist sicher und umkehrbar. Paritätsdisk werden verwendet, um Ihre Daten vor Datenträger-Ausfällen zu schützen. Wenn Sie ein Paritätsdisk erstellen, fügen Sie die zu schützenden Datenträger hinzu. Fügen Sie keine Datenträger hinzu, die das System oder eine anderer Paritätsdisk enthält.", + "mgmt.storage.snapraid.min1parity": "Mindestens 1 Paritätsdisk auswählen", + "mgmt.storage.snapraid.min2datadisks": "Wählen Sie mindestens 2 Datenträger", + "mgmt.storage.snapraid.min3chars": "Der Name sollte mindestens 3 Zeichen lang sein", + "mgmt.storage.snapraid.notAlphanumeric": "Name sollte alphanumerisch sein", + "mgmt.storage.snapraid.removeDatadisk": "Datenträger entfernen", + "mgmt.storage.snapraid.scrubInterval.scrubIntervalLabel": "Zeitplan Check Datenintegrität", + "mgmt.storage.snapraid.storageParity": "Speicherparität", + "mgmt.storage.snapraid.syncInterval.syncIntervalLabel": "Zeitplan Sync Paritätsdisks", + "mgmt.storage.startFormatLog": "Formatieren des Datenträgers {{disk}} wird gestartet...", + "mgmt.storage.syncScrubIntervalTitle": "Sync/Scrub Intervalle", + "mgmt.storage.typeTitle": "Typ", + "mgmt.storage.unMountDiskButton": "Datenträger {{unMount}}", + "mgmt.storage.unMountDiskText": "Sie sind dabei, die Festplatte {{disk}}{{mountpoint}} zu {{unMount}}. Dadurch wird der Inhalt {{unMount}} im Dateiexplorer angezeigt. Permanentes {{unMount}} bleibt nach dem Neustart bestehen.", + "mgmt.storage.unMountText": "Sie sind dabei, einen Ordner {{mountpoint}} zu {{unMount}}. Dadurch wird der Inhalt {{unAvailable}}, um im Datei-Explorer angezeigt zu werden. Permanentes {{unMount}} bleibt nach dem Neustart bestehen.", + "mgmt.storage.unavailable": "unverfügbar", + "mgmt.urls.edit.advancedSettings.advancedSettingsInfo": "Diese Einstellungen sind nur für fortgeschrittene Benutzer gedacht. Bitte ändern Sie diese nicht, wenn Sie nicht wissen, was Sie tun.", + "mgmt.urls.edit.advancedSettings.filterIpWarning": "Mit dieser Einstellung werden alle Anfragen herausgefiltert, die nicht von den angegebenen IPs stammen. Dies erfordert, dass Ihr Setup die wahre IP des Clients meldet. Standardmäßig wird dies der Fall sein, aber einige exotische Einstellungen (wie die Installation von docker/Cosmos unter Windows oder hinter Cloudlfare) verhindern, dass Cosmos die wahre IP des Clients kennt. Wenn Sie oben \"Restrict to Constellation\" verwendet haben, werden Constellation-IPs unabhängig von dieser Einstellung immer zugelassen.", + "mgmt.urls.edit.advancedSettings.hideFromDashboardCheckbox.hideFromDashboardLabel": "Aus dem Dashboard ausblenden", + "mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderLabel": "Host-Header überschreiben (verwenden Sie dies, um Anfragen von einem anderen Server/IP aufzulösen)", + "mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderPlaceholder": "Host-Header überschreiben", + "mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpLabel": "Whitelist für Eingehende IPs und/oder IP-Bereiche (durch Komma getrennt)", + "mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpPlaceholder": "Whitelist für Eingehende IPs und/oder IP-Bereiche (durch Komma getrennt)", + "mgmt.urls.edit.advancedSettingsTitle": "Erweiterte Einstellungen", + "mgmt.urls.edit.basicSecurity.authEnabledCheckbox.authEnabledLabel": "Authentifizierung erforderlich", + "mgmt.urls.edit.basicSecurity.restrictToConstellationCheckbox.restrictToConstellationLabel": "Zugang zu Constellation VPN einschränken", + "mgmt.urls.edit.basicSecurity.smartShieldEnabledCheckbox.smartShieldEnabledLabel": "Smart Shield Absicherung", + "mgmt.urls.edit.basicSecurityTitle": "Grundlegende Sicherheit", + "mgmt.urls.edit.insecureHttpsCheckbox.insecureHttpsLabel": "Unsicheres HTTPS-Ziel akzeptieren (nicht empfohlen)", + "mgmt.urls.edit.newUrlTitle": "neue URL", + "mgmt.urls.edit.pathPrefixInputx.pathPrefixLabel": "Dateipfad-Präfix", + "mgmt.urls.edit.pathPrefixInputx.pathPrefixPlaceholder": "Dateipfad-Präfix", + "mgmt.urls.edit.sourceInfo": "Über welche URL möchten Sie auf Ihr Ziel zugreifen?", + "mgmt.urls.edit.stripPathCheckbox.stripPathLabel": "Entferne Dateipfad-Präfix", + "mgmt.urls.edit.targetFolderPathInput.targetFolderPathLabel": "Zielverzeichnis", + "mgmt.urls.edit.targetSettings.targetUrlInput.targetUrlLabel": "Ziel-URL", + "mgmt.urls.edit.targetSettingsTitle": "Zieleinstellungen", + "mgmt.urls.edit.targetType.modeSelection.modeLabel": "Modus", + "mgmt.urls.edit.targetType.modeSelection.proxyChoice": "Proxy", + "mgmt.urls.edit.targetType.modeSelection.redirectChoice": "Weiterleitung", + "mgmt.urls.edit.targetType.modeSelection.servAppChoice": "ServApp - Docker Container", + "mgmt.urls.edit.targetType.modeSelection.spaChoice": "App als Einzelseite", + "mgmt.urls.edit.targetType.modeSelection.staticChoice": "Statischer Ordner", + "mgmt.urls.edit.targetTypeInfo": "Was wollen Sie mit dieser Route erreichen?", + "mgmt.urls.edit.targetTypeTitle": "Zieltyp", + "mgmt.urls.edit.useHostCheckbox.useHostLabel": "Verwende Host-Adresse", + "mgmt.urls.edit.usePathPrefixCheckbox.usePathPrefixLabel": "Verwende Dateipfad-Präfix", + "mgmt.usermgmt.adminLabel": "Administrator", + "mgmt.usermgmt.createUser.emailOptInput.emailOptLabel": "E-Mail-Adresse (Optional)", + "mgmt.usermgmt.createUserTitle": "Benutzer anlegen", + "mgmt.usermgmt.deleteUserConfirm": "Sind Sie sicher, dass Sie den Benutzer löschen wollen: ", + "mgmt.usermgmt.deleteUserTitle": "Benutzer löschen", + "mgmt.usermgmt.editEmail.emailInput.emailLabel": "E-Mail-Adresse", + "mgmt.usermgmt.editEmailText": "Verwenden Sie dieses Formular, um zur Bearbeitung von {{user}}'s Email einzuladen.", + "mgmt.usermgmt.editEmailTitle": "E-Mail-Adresse bearbeiten", + "mgmt.usermgmt.inviteExpiredLabel": "Einladung abgelaufen", + "mgmt.usermgmt.invitePendingLabel": "Einladung Pendent", + "mgmt.usermgmt.inviteUser.emailSentAltShare": "Senden Sie diesen Link an", + "mgmt.usermgmt.inviteUser.emailSentAltShareLink": "Alternativ können Sie auch folgenden Link weitergeben:", + "mgmt.usermgmt.inviteUser.emailSentAltShareTo": "an", + "mgmt.usermgmt.inviteUser.emailSentConfirmation": "Eine E-Mail wurde versandt", + "mgmt.usermgmt.inviteUser.emailSentwithLink": "mit einem Link zu", + "mgmt.usermgmt.inviteUserText": "Verwenden Sie dieses Formular, um einen neuen Benutzer in das System einzuladen.", + "mgmt.usermgmt.inviteUserTitle": "Benutzer einladen", + "mgmt.usermgmt.lastLogin": "Zuletzt eingeloggt", + "mgmt.usermgmt.reset2faButton": "2FA Zurücksetzen", + "mgmt.usermgmt.sendPasswordResetButton": "Passwort-Reset versenden", + "navigation.home.Avx": "AVX wird unterstützt", + "navigation.home.LetsEncryptEmailError": "Sie haben Let's Encrypt für das automatische HTTPS-Zertifikat aktiviert. Sie müssen die Konfiguration mit einer E-Mail-Adresse versehen, die für Let's Encrypt in den Configs verwendet werden soll.", + "navigation.home.LetsEncryptError": "Es gibt Fehler in Ihrer Let's Encrypt-Konfiguration oder in einer Ihrer Routen, bitte beheben Sie diese so schnell wie möglich:", + "navigation.home.availRam": "verfügb.", + "navigation.home.configChangeRequiresRestartError": "Sie haben Änderungen an der Konfiguration vorgenommen, die einen Neustart erfordern, um wirksam zu werden. Bitte starten Sie Cosmos neu, um die Änderungen zu übernehmen.", + "navigation.home.cosmosNotDockerHostError": "Ihr Cosmos-Server läuft nicht im Docker-Host-Netzwerkmodus. Es wird empfohlen, dass Sie Ihre Installation migrieren.", + "navigation.home.dbCantConnectError": "Die Datenbank kann sich nicht verbinden, dies hat Auswirkungen auf mehrere Funktionen von Cosmos. Bitte so schnell wie möglich beheben!", + "navigation.home.localhostnotRecommendedError": "Sie verwenden localhost oder 0.0.0.0 als Hostname in der Konfiguration. Es wird empfohlen, stattdessen einen Domänennamen oder eine IP zu verwenden.", + "navigation.home.network": "NETZWERK", + "navigation.home.newCosmosVersionError": "Eine neue Version von Cosmos ist verfügbar! Bitte aktualisieren Sie auf die neueste Version, um die neuesten Funktionen und Fehlerbehebungen zu erhalten.", + "navigation.home.noApps": "Sie haben keine Anwendungen konfiguriert. Bitte fügen Sie einige Anwendungen im Konfigurationsbereich hinzu.", + "navigation.home.noAppsTitle": "kein Apps", + "navigation.home.noAvx": "AVX wird nicht unterstützt", + "navigation.home.rcvNet": "rx", + "navigation.home.trsNet": "tx", + "navigation.home.usedRam": "in verw.", + "navigation.market.applicationsTitle": "Applikationen", + "navigation.market.compose": "Compose", + "navigation.market.filterDuplicateCheckbox": "Duplikate Filtern", + "navigation.market.image": "Image", + "navigation.market.newSources.additionalMarketsInfo": "Dadurch können Sie zusätzliche App-Stores von Drittanbietern in den Markt aufnehmen.", + "navigation.market.newSources.additionalMarketsInfo.href": "Hier Starten", + "navigation.market.newSources.additionalMarketsInfo.moreInfo": "starten Sie hier, um neue Quellen zu finden:", + "navigation.market.repository": "Repository", + "navigation.market.search": "Suche in {{count}} Apps...", + "navigation.market.sources.addSourceButton": "Quelle Hinzufügen", + "navigation.market.sources.editSourcesButton": "Quellen", + "navigation.market.sources.nameNotUniqueValidation": "Der Name muss eindeutig sein", + "navigation.market.sources.urlRequiredValidation": "URL ist erforderlich", + "navigation.market.sourcesTitle": "Quellen bearbeiten", + "navigation.market.startServAppButton": "ServApp starten", + "navigation.market.unofficialMarketTooltip": "Diese App wird nicht im Cosmos-Cloud Appstore gehostet. Sie ist nicht offiziell verifiziert und getestet.", + "navigation.market.viewButton": "Ansehen", + "navigation.monitoring.alerts.action.edit": "Alarm bearbeiten", + "navigation.monitoring.alerts.action.edit.actionTypeInput.actionTypeLabel": "Aktionstyp", + "navigation.monitoring.alerts.action.edit.conditionOperator.validation": "Bedingungsoperator ist erforderlich", + "navigation.monitoring.alerts.action.edit.conditionValue.validation": "Bedingungswert ist erforderlich", + "navigation.monitoring.alerts.action.edit.period.validation": "Zeitraum ist erforderlich", + "navigation.monitoring.alerts.action.edit.severitySelection.severityLabel": "Schweregrad", + "navigation.monitoring.alerts.action.edit.trackingMetric.validation": "zu verfolgende Metrik ist erforderlich", + "navigation.monitoring.alerts.actions.restart": "Neustart des Containers, der den Alert verursacht hat", + "navigation.monitoring.alerts.actions.restartActionInfo": "Mit der Aktion Neustart wird versucht, alle mit der Metrik verbundenen Container neu zu starten. Dies wirkt sich nur auf ressourcenspezifische Metriken aus (z.B. CPU eines bestimmten Containers). Es hat keine Auswirkung auf globale Metriken wie z.B. die global genutzte CPU.", + "navigation.monitoring.alerts.actions.sendEmail": "Eine E-Mail senden", + "navigation.monitoring.alerts.actions.sendNotification": "Eine Benachrichtigung senden", + "navigation.monitoring.alerts.actions.stop": "Stoppen/Deaktivieren von Ressourcen, die den Alarm verursachen", + "navigation.monitoring.alerts.actions.stopActionInfo": "Mit der Aktion \"Stop\" wird versucht, alle Ressourcen (z. B. Container, Routen usw.) zu stoppen/deaktivieren, die mit der Metrik verknüpft sind. Dies wirkt sich nur auf ressourcenspezifische Metriken aus (z. B. die CPU eines bestimmten Containers). Es hat keine Auswirkung auf globale Metriken wie z.B. die global genutzte CPU.", + "navigation.monitoring.alerts.actionsTitle": "Aktionen", + "navigation.monitoring.alerts.alertNameLabel": "Alarmbezeichnung", + "navigation.monitoring.alerts.astTriggeredTitle": "Zuletzt ausgelöst", + "navigation.monitoring.alerts.conditionLabel": "Bedingung ist ein Prozentsatz des Maximalwerts", + "navigation.monitoring.alerts.conditionOperatorLabel": "Bedingungsoperator", + "navigation.monitoring.alerts.conditionTitle": "Bedingung", + "navigation.monitoring.alerts.conditionValueLabel": "Bedingungswert", + "navigation.monitoring.alerts.newAlertButton": "Neuer Alarm", + "navigation.monitoring.alerts.periodLabel": "Zeitraum (wie oft soll die Metrik überprüft werden)", + "navigation.monitoring.alerts.periodTitle": "Zeitraum", + "navigation.monitoring.alerts.resetToDefaultButton": "Auf Standardwerte zurücksetzen", + "navigation.monitoring.alerts.throttleCheckbox.throttleLabel": "Drosseln (wird nur maximal einmal pro Tag ausgelöst)", + "navigation.monitoring.alerts.trackingMetricLabel": "Zu verfolgende Metrik", + "navigation.monitoring.alerts.trackingMetricTitle": "verfolgte Metrik", + "navigation.monitoring.alertsTitle": "Warnmeldungen", + "navigation.monitoring.daily": "Täglich", + "navigation.monitoring.events.datePicker.fromLabel": "Von", + "navigation.monitoring.events.datePicker.toLabel": "Bis", + "navigation.monitoring.events.eventsFound": "{{count}} Ereignis gefunden vom {{from}} bis {{to}}", + "navigation.monitoring.events.eventsFound_other": "{{count}} Ereignisse gefunden vom {{from}} bis {{to}}", + "navigation.monitoring.events.eventsFound_zero": "Keine Ereignisse gefunden vom {{from}} bis {{to}}", + "navigation.monitoring.events.loadMoreButton": "mehr laden", + "navigation.monitoring.events.searchInput.searchPlaceholder": "Suche (Text oder bson)", + "navigation.monitoring.eventsTitle": "Ereignisse", + "navigation.monitoring.hourly": "Stündlich", + "navigation.monitoring.latest": "Zuletzt", + "navigation.monitoring.proxyTitle": "Proxy", + "navigation.monitoring.resourceDashboard.averageNetworkTitle": "Container - Durchschnittliches Netzwerk", + "navigation.monitoring.resourceDashboard.averageResourcesTitle": "Container - Durchschnittliche Ressourcen", + "navigation.monitoring.resourceDashboard.blockReasonTitle": "Gründe für blockierte Anfragen", + "navigation.monitoring.resourceDashboard.blockedRequestsTitle": "Blockierte Anfragen", + "navigation.monitoring.resourceDashboard.diskUsageTitle": "Festplattennutzung", + "navigation.monitoring.resourceDashboard.reasonByBots": "Bots", + "navigation.monitoring.resourceDashboard.reasonByGeo": "Nach Geolokalisierung (gesperrte Länder)", + "navigation.monitoring.resourceDashboard.reasonByHostname": "Nach Hostname (normalerweise IP-Scan-Bedrohung)", + "navigation.monitoring.resourceDashboard.reasonByRef": "Nach Referent", + "navigation.monitoring.resourceDashboard.reasonBySmartShield": "Smart Shield (verschiedene Missbrauchsmetriken wie Zeit, Größe, Brute-Force, gleichzeitige Anfragen, usw...). Es beinhaltet keine Sperrung für gesperrte IP, um Ressourcen im Falle von potenziellen Angriffen zu sparen.", + "navigation.monitoring.resourceDashboard.reasonByWhitelist": "Nach IP-Whitelists (einschließlich der auf Constellation beschränkten)", + "navigation.monitoring.resourceDashboard.requestsPerUrlTitle": "Anfragen pro URLs", + "navigation.monitoring.resourceDashboard.requestsTitle": "Ressourcenanfragen", + "navigation.monitoring.resourceDashboard.responsesTitle": "Ressourcenantworten", + "navigation.monitoring.resourcesTitle": "Ressourcen", + "navigation.monitoringTitle": "Server-Überwachung", + "newInstall.LetsEncrypt.cloudflareWarning": "Wenn Sie Cloudflare verwenden, stellen Sie sicher, dass der DNS-Eintrag NICHT auf Proxied eingestellt ist (Sie sollten keine orangefarbene Wolke, sondern eine graue Wolke sehen). Andernfalls erlaubt Cloudflare Let's Encrypt nicht, Ihre Domain zu verifizieren.
Alternativ können Sie auch die DNS-Herausforderung verwenden.", + "newInstall.LetsEncrypt.dnsChallengeInfo": "Sie haben die DNS-Challenge aktiviert. Stellen Sie sicher, dass Sie die Umgebungsvariablen für Ihren DNS-Anbieter gesetzt haben. Sie können es jetzt aktivieren, aber stellen Sie sicher, dass Sie Ihre API-Tokens entsprechend eingerichtet haben, bevor Sie versuchen, nach diesem Installationsprogramm auf Cosmos zuzugreifen. Sehen Sie das Dokument hier: <1>https://go-acme.github.io/lego/dns/", + "newInstall.adminAccountText": "Erstellen Sie ein lokales Administratorkonto zur Verwaltung Ihres Servers. E-Mail ist optional und wird für Benachrichtigungen und Passwortwiederherstellung verwendet.", + "newInstall.adminAccountTitle": "Admin-Konto 🔑 (Schritt 4/4)", + "newInstall.applyRestartAction": "Anwenden und neu starten", + "newInstall.checkInputValidation": "Bitte überprüfen Sie, ob Sie alle Eingaben korrekt ausgefüllt haben", + "newInstall.cleanInstallCheckbox": "Saubere Installation (bestehende Konfiguration löschen)", + "newInstall.dbConnected": "Die Datenbank ist verbunden.", + "newInstall.dbInstalling": "Datenbank wird installiert...", + "newInstall.dbNotConnected": "Die Datenbank ist nicht verbunden.", + "newInstall.dbSelection.createChoice": "Automatisches Erstellen einer sicheren Datenbank (empfohlen)", + "newInstall.dbSelection.dbLabel": "Bitte treffen Sie eine Auswahl", + "newInstall.dbSelection.disabledChoice": "Benutzerverwaltung und UI deaktivieren", + "newInstall.dbSelection.providedChoice": "Eigene Datenbankzugangsdaten bereitstellen", + "newInstall.dbText": "Cosmos verwendet eine MongoDB-Datenbank, um alle Daten zu speichern. Die Datenbank ist optional, aber die Authentifizierung und die Benutzeroberfläche funktionieren nicht ohne eine Datenbank.", + "newInstall.dbTitle": "Datenbank 🗄️ (Schritt 2/4)", + "newInstall.dbUrlInput.dbUrlLabel": "Datenbankadresse (URL)", + "newInstall.dockerAvail": "Docker ist installiert und läuft.", + "newInstall.dockerChecking": "Docker-Status erneut prüfen...", + "newInstall.dockerNotConnected": "Docker ist nicht verbunden! Bitte überprüfen Sie Ihre Docker-Verbindung.
Haben Sie vergessen,
-v /var/run/docker.sock:/var/run/docker.sock
zu Ihrem Docker-Run-Befehl hinzuzufügen?
Wenn Ihr Docker-Daemon an einem anderen Ort läuft, fügen Sie bitte
-e DOCKER_HOST=...
zu Ihrem Docker-Startbefehl hinzu.", + "newInstall.dockerTitle": "Docker 🐋 (Schritt 1/4)", + "newInstall.finishText": "Gut gemacht! Sie haben Cosmos erfolgreich installiert. Sie können sich jetzt mit dem von Ihnen erstellten Administratorkonto auf Ihrem Server anmelden. Wenn Sie den Hostnamen geändert haben, vergessen Sie nicht, diese URL zu verwenden, um nach dem Neustart auf Ihren Server zuzugreifen. Wenn Sie Probleme haben, überprüfen Sie die Protokolle auf Fehlermeldungen und bearbeiten Sie die Datei im Ordner /config. Wenn Sie es immer noch nicht schaffen, treten Sie bitte unserem <0>Discord-Server bei und wir helfen Ihnen gerne!", + "newInstall.finishTitle": "Abgeschlossen 🎉", + "newInstall.fqdnAutoLetsEncryptInfo": "Sie scheinen einen Domänennamen zu verwenden.
Let's Encrypt kann automatisch ein Zertifikat für Sie erstellen.", + "newInstall.hostnameInput.hostnameLabel": "Hostname (Wie möchten Sie auf Cosmos zugreifen?)", + "newInstall.hostnameInput.hostnamePlaceholder": "IhreDomain.com, Ihre IP oder localhost", + "newInstall.hostnamePointsToInfo": "Dieser Hostname verweist auf {{hostIp}}, überprüfen Sie, ob es sich dabei um Ihre Server-IP handelt!", + "newInstall.httpsText": "Es wird empfohlen, Let's Encrypt zu verwenden, um automatisch HTTPS-Zertifikate bereitzustellen. Dazu ist ein gültiger Domänenname erforderlich, der auf diesen Server verweist. Wenn Sie keinen haben, können Sie im Dropdown-Menü die Option \"Selbstsigniertes Zertifikat generieren\" wählen. Wenn Sie HTTPS aktivieren, wird es nach dem nächsten Neustart wirksam.", + "newInstall.httpsTitle": "HTTPS 🌐 (Schritt 3/4)", + "newInstall.letsEncryptChoiceOnlyfqdnValidation": "Let\\'s Encrypt akzeptiert nur Domainnamen", + "newInstall.linkToDocs": "Link zur Dokumentation", + "newInstall.loading": "wird geladen", + "newInstall.localAutoSelfSignedInfo": "Sie scheinen eine IP-Adresse oder eine lokale Domäne zu verwenden.
Sie können automatische selbstsignierte Zertifikate verwenden.", + "newInstall.previousButton": "Zurück", + "newInstall.privCertInput.privCertLabel": "Privates Zertifikat", + "newInstall.pubCertInput.pubCertLabel": "Öffentliches Zertifikat", + "newInstall.setupUser.nicknameRootAdminNotAllowedValidation": "Nickname cannot be admin or root", + "newInstall.setupUser.passwordMustMatchValidation": "Passwords must match", + "newInstall.skipAction": "Überspringen", + "newInstall.sslEmailInput.sslEmailLabel": "Let's Encrypt E-Mail", + "newInstall.usermgmt.disableButton": "Deaktivieren", + "newInstall.usermgmt.inviteUser.resendInviteButton": "Einladung erneut senden", + "newInstall.welcomeText": "Zunächst einmal vielen Dank, dass Sie Cosmos ausprobiert haben! Und willkommen beim Einrichtungsassistenten. Dieser Assistent wird Sie durch die Einrichtung von Cosmos führen. Es dauert ca. 2-3 Minuten und Sie sind bereit, loszulegen.", + "newInstall.welcomeTitle": "Willkommen! 💖", + "newInstall.whatIsCosmos": "Cosmos verwendet Docker, um Anwendungen auszuführen. Docker ist optional, wenn Cosmos keine Verbindung zu Docker herstellen kann, wird es im Reverse-Proxy-Modus ausgeführt.", + "newInstall.wildcardLetsEncryptCheckbox.wildcardLetsEncryptLabel": "Verwenden Sie Wildcard-Zertifikate für *.", + "newInstall.wildcardLetsEncryptError": "Sie haben Wildcard-Zertifikate mit Let's Encrypt aktiviert. Dies funktioniert nur, wenn Sie die DNS-Herausforderung verwenden! Bitte bearbeiten Sie die DNS Provider Texteingabe." +} \ No newline at end of file diff --git a/client/src/utils/locales/en/translation.json b/client/src/utils/locales/en/translation.json new file mode 100644 index 00000000..10fa7700 --- /dev/null +++ b/client/src/utils/locales/en/translation.json @@ -0,0 +1,674 @@ +{ + "Storage": "Storage", + "auth.accountUnconfirmedError": "You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.", + "auth.confirmPassword": "Confirm Password", + "auth.enterPwd": "Enter your password", + "auth.forgotPassword.backToLogin": "Back to Login", + "auth.forgotPassword.checkEmail": "Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.", + "auth.forgotPassword.resetPassword": "Reset Password", + "auth.forgotPwd": "Forgot Your Password?", + "auth.genPwdStrength.good": "Good", + "auth.genPwdStrength.normal": "Normal", + "auth.genPwdStrength.poor": "Poor", + "auth.genPwdStrength.strong": "Strong", + "auth.genPwdStrength.weak": "Weak", + "auth.hostnameInput": "Set your hostname first", + "auth.loggedOutError": "You have been disconnected. Please login to continue", + "auth.login": "Login", + "auth.logoffText": "You have been logged off. Redirecting you...", + "auth.notAdminError": "You need to be Admin", + "auth.notLoggedInError": "You need to be logged in to access this", + "auth.pwd": "Password", + "auth.pwdRequired": "Password is required", + "auth.pwdResetNotAllowed": "This server does not allow password reset.", + "auth.selectOption": "Select an option", + "auth.unexpectedErrorValidation": "Unexpected error. Check your infos or try again later.", + "auth.usernameInput": "Enter your nickname", + "auth.wrongCredError": "Wrong nickname or password. Try again or try resetting your password", + "auth.yourPassword": "Your Password", + "global.CPU": "CPU", + "global.RAM": "RAM", + "global.addAction": "Add", + "global.backAction": "Back", + "global.cancelAction": "Cancel", + "global.close": "Close", + "global.confirmAction": "Confirm", + "global.confirmDeletion": "Are you sure?", + "global.copyFilenameSuffix": "Copy", + "global.createAction": "Create", + "global.createdAt": "Created At", + "global.delete": "Delete", + "global.description": "Description", + "global.driver": "Driver", + "global.edit": "Edit", + "global.emailInvalidValidation": "Must be a valid email", + "global.emailRequiredValidation": "Email is required", + "global.enabled": "Enabled", + "global.error": "Error", + "global.hostname": "Hostname", + "global.logout": "Logout", + "global.mount": "Mount", + "global.name.validation": "Name is required", + "global.nameTitle": "Name", + "global.network": "Network", + "global.networks": "Networks", + "global.never": "Never", + "global.next": "Next", + "global.nicknameLabel": "Nickname", + "global.nicknameRequiredValidation": "Nickname is required", + "global.refresh": "Refresh", + "global.refreshPage": "Refresh Page", + "global.required": "Required", + "global.resetZoomButton": "Reset Zoom", + "global.saveAction": "Save", + "global.savedConfirmation": "Saved!", + "global.savedError": "Error while saving, try again.", + "global.searchPlaceholder": "Search...", + "global.securityTitle": "Security", + "global.source": "Source", + "global.statusTitle": "Status", + "global.success": "Success", + "global.target": "Target", + "global.temperature": "Temperature", + "global.time": "Time", + "global.unmount": "Unmount", + "global.update": "Update", + "global.user": "User", + "global.volume": "Volume", + "header.notification.message.alertTriggered": "The alert \"{{Vars}}\" was triggered.", + "header.notification.message.certificateRenewed": "The TLS certificate for the following domains has been renewed: {{Vars}}", + "header.notification.message.containerUpdate": "Container {{Vars}} updated to the latest version!", + "header.notification.title.alertTriggered": "Alert triggered", + "header.notification.title.certificateRenewed": "Cosmos Certificate Renewed", + "header.notification.title.containerUpdate": "Container Update", + "header.notification.title.serverError": "Server Error", + "header.notificationTitle": "Notification", + "header.profileLabel": "Profile", + "header.settingLabel": "Setting", + "menu-items.management.configurationTitle": "Configuration", + "menu-items.management.constellation": "Constellation", + "menu-items.management.openId": "OpenID", + "menu-items.management.schedulerTitle": "Scheduler", + "menu-items.management.servApps": "ServApps", + "menu-items.management.storage": "Storage", + "menu-items.management.urls": "URLs", + "menu-items.management.usersTitle": "Users", + "menu-items.managementTitle": "Management", + "menu-items.navigation": "Navigation", + "menu-items.navigation.home": "Home", + "menu-items.navigation.marketTitle": "Market", + "menu-items.navigation.monitoringTitle": "Monitoring", + "menu-items.support": "Support", + "menu-items.support.bugReportTitle": "Found a Bug?", + "menu-items.support.discord": "Discord", + "menu-items.support.docsTitle": "Documentation", + "menu-items.support.github": "Github", + "mgmt.config.appearance.appDetailsOnHomepageCheckbox.appDetailsOnHomepageLabel": "Show Application Details on Homepage", + "mgmt.config.appearance.primaryColorSlider": "Primary Color", + "mgmt.config.appearance.resetColorsButton.resetColorsLabel": "Reset Colors", + "mgmt.config.appearance.resetWallpaperButton.resetWallpaperLabel": "Reset Wallpaper", + "mgmt.config.appearance.secondaryColorSlider": "Secondary Color", + "mgmt.config.appearance.uploadWallpaperButton.previewBrokenError": "preview seems broken. Please re-upload.", + "mgmt.config.appearance.uploadWallpaperButton.uploadWallpaperLabel": "Upload Wallpaper", + "mgmt.config.appearanceTitle": "mgmt.config.appearanceTitle", + "mgmt.config.certRenewalLinktext": "this link to the documentation", + "mgmt.config.certRenewalText": "You are using Let's Encrypt but you are not using the DNS Challenge with a wildcard certificate. This means the server has to renew the certificate everytime you add a new hostname, causing a few seconds of downtime. To avoid it in the future, please refer to", + "mgmt.config.certRenewalTitle": "Certificate Renewal", + "mgmt.config.containerPicker.containerNameSelection.containerNameLabel": "Container Name", + "mgmt.config.containerPicker.containerNameSelection.containerNameValidation": "Please select a container", + "mgmt.config.containerPicker.containerPortInput": "Container Port", + "mgmt.config.containerPicker.containerPortSelection.containerPortValidation": "Please enter a port", + "mgmt.config.containerPicker.containerProtocolInput": "Container Protocol (use HTTP if unsure, or tcp for non-http proxying)", + "mgmt.config.containerPicker.targetTypePreview": "Result Target Preview", + "mgmt.config.containerPicker.targetTypePreview.targetTypePreviewLabel": "This will be generated automatically", + "mgmt.config.containerPicker.targetTypeValidation.noPort": "Invalid Target, must have a port", + "mgmt.config.containerPicker.targetTypeValidation.wrongProtocol": "Invalid Target, must start with http:// or https://", + "mgmt.config.docker.defaultDatapathInput.defaultDatapathLabel": "Default data path for installs", + "mgmt.config.docker.skipPruneImageCheckbox.skipPruneImageLabel": "Do not clean up Images", + "mgmt.config.docker.skipPruneNetworkCheckbox.skipPruneNetworkLabel": "Do not clean up Network", + "mgmt.config.email.enableCheckbox.enableHelperText": "Enable SMTP", + "mgmt.config.email.enableCheckbox.enableLabel": "Enable SMTP", + "mgmt.config.email.inbobox.label": "This allow you to setup an SMTP server for Cosmos to send emails such as password reset emails and invites", + "mgmt.config.email.passwordInput.passwordHelperText": "SMTP Password", + "mgmt.config.email.passwordInput.passwordLabel": "SMTP Password", + "mgmt.config.email.selfSignedCheckbox.SelfSignedHelperText": "Allow self-signed certificate", + "mgmt.config.email.selfSignedCheckbox.SelfSignedLabel": "Allow Insecure TLS", + "mgmt.config.email.senderInput.senderHelperText": "SMTP From", + "mgmt.config.email.senderInput.senderLabel": "SMTP From", + "mgmt.config.email.tlsCheckbox.tlsLabel": "SMTP Uses TLS", + "mgmt.config.email.usernameInput.usernameHelperText": "SMTP Username", + "mgmt.config.email.usernameInput.usernameLabel": "SMTP Username", + "mgmt.config.general.backupDirInput.backupDirHelperText": "Directory where backups will be stored (relative to the host server `/`)", + "mgmt.config.general.backupDirInput.backupDirLabel": "Backup Output Directory (relative to the host server `/`)", + "mgmt.config.general.configFileInfo": "This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.", + "mgmt.config.general.forceMfaCheckbox.forceMfaHelperText": "Require MFA for all users", + "mgmt.config.general.forceMfaCheckbox.forceMfaLabel": "Force Multi-Factor Authentication", + "mgmt.config.general.logLevelInput": "Level of logging (Default: INFO)", + "mgmt.config.general.logLevelInput.logLevelValidation": "Logging Level is required", + "mgmt.config.general.mongoDbInput": "MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)", + "mgmt.config.general.monitoringCheckbox.monitoringLabel": "Monitoring Enabled", + "mgmt.config.general.notAdminWarning": "As you are not an admin, you can't edit the configuration.
This page is only here for visibility.", + "mgmt.config.general.puppetMode.configVolumeInput.configVolumeHelperText": "Puppet Mode Config Volume", + "mgmt.config.general.puppetMode.configVolumeInput.configVolumeLabel": "Puppet Mode Config Volume", + "mgmt.config.general.puppetMode.dbVolumeInput.dbVolumeHelperText": "Puppet Mode Database Volume", + "mgmt.config.general.puppetMode.dbVolumeInput.dbVolumeLabel": "Puppet Mode Database Volume", + "mgmt.config.general.puppetMode.enableCheckbox.enableHelperText": "Enable Puppet Mode", + "mgmt.config.general.puppetMode.enableCheckbox.enableLabel": "Puppet Mode Enabled", + "mgmt.config.general.puppetMode.hostnameInput.hostnameHelperText": "Puppet Mode Hostname", + "mgmt.config.general.puppetMode.hostnameInput.hostnameLabel": "Puppet Mode Hostname", + "mgmt.config.general.puppetMode.passwordInput.passwordHelperText": "Puppet Mode Password", + "mgmt.config.general.puppetMode.passwordInput.passwordLabel": "Puppet Mode Password", + "mgmt.config.general.puppetMode.usernameInput.usernameHelperText": "Puppet Mode Username", + "mgmt.config.general.puppetMode.usernameInput.usernameLabel": "Puppet Mode Username", + "mgmt.config.general.puppetMode.versionInput.versionHelperText": "Puppet Mode Version", + "mgmt.config.general.puppetMode.versionInput.versionLabel": "Puppet Mode Version", + "mgmt.config.general.puppetModeTitle": "Puppet Mode", + "mgmt.config.generalTitle": "General", + "mgmt.config.header.purgeMetricsButton.purgeMetricsLabel": "Purge Metrics Dashboard", + "mgmt.config.header.purgeMetricsButton.purgeMetricsPopUp.cofirmAction": "Are you sure you want to purge all the metrics data from the dashboards?", + "mgmt.config.header.refreshButton.refreshLabel": "Refresh", + "mgmt.config.header.restartButton.restartLabel": "Restart Server", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessLabel": "Allow insecure access via local IP", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessTooltip": "When HTTPS is used along side a domain, depending on your networking configuration, it is possible that your server is not receiving direct local connections.
This option allows you to also access your Cosmos admin using your local IP address, like ip:port.
You can already create ip:port URLs for your apps, but this will make them HTTP-only.", + "mgmt.config.http.allowInsecureLocalAccessCheckbox.allowInsecureLocalAccessWarning": "This option is not recommended as it exposes your server to security risks on your local network.
Your local network is safer than the internet, but not safe, as devices like IoTs, smart-TVs, smartphones or even your router can be compromised.
If you want to have a secure offline / local-only access to a server that uses a domain name and HTTPS, use Constellation instead.", + "mgmt.config.http.allowSearchIndexCheckbox": "Enable this option if you have a public site and want to allow search engines to find it, so it appears on search results. ", + "mgmt.config.http.allowSearchIndexCheckbox.allowSearchIndexLabel": "Allow search engines to index your server", + "mgmt.config.http.hostnameInput.HostnameLabel": "Hostname: This will be used to restrict access to your Cosmos Server (Your IP, or your domain name)", + "mgmt.config.http.hostnameInput.HostnameValidation": "Hostname is required", + "mgmt.config.proxy.noRoutesConfiguredText": "No routes configured.", + "mgmt.config.proxy.originTitle": "Origin", + "mgmt.config.proxy.refreshNeededWarning.notDomain": "You are also not using a domain name, the server might go offline for a few seconds to remap your docker ports.", + "mgmt.config.proxy.refreshNeededWarning.selfSigned": "You need to refresh the page because you are using a self-signed certificate, in case you have to accept any new certificates. To avoid it in the future, please use Let's Encrypt. {{isNotDomain && 'You are also not using a domain name, the server might go offline for a few seconds to remap your docker ports.'}}", + "mgmt.config.proxy.saveChangesButton": "Save Changes", + "mgmt.config.proxy.urlTitle": "URL", + "mgmt.config.restart.laterButton": "Later", + "mgmt.config.restart.okButton": "OK", + "mgmt.config.restart.restartQuestion": "Do you want to restart your server?", + "mgmt.config.restart.restartStatus": "Restarting Server...", + "mgmt.config.restart.restartTimeoutWarning": "The server is taking longer than expected to restart.", + "mgmt.config.restart.restartTimeoutWarningTip": "Consider troubleshouting the logs. If you use a self-signed certificate, you might have to refresh and re-accept it.", + "mgmt.config.restart.restartTitle": "Restart Server?", + "mgmt.config.security.adminRestrictions.adminConstellationCheckbox.adminConstellationLabel": "Only allow access to the admin panel from the constellation", + "mgmt.config.security.adminRestrictions.adminRestrictionsInfo": "Use those options to restrict access to the admin panel. Be careful, if you lock yourself out, you will need to manually edit the config file. To restrict the access to your local network, you can use the 'Admin Whitelist' with the IP range 192.168.0.0/16", + "mgmt.config.security.adminRestrictions.adminWhitelistInput.adminWhitelistHelperText": "Comma separated list of IPs that will be allowed to access the admin panel", + "mgmt.config.security.adminRestrictions.adminWhitelistInput.adminWhitelistLabel": "Admin Whitelist Inbound IPs and/or IP ranges (comma separated)", + "mgmt.config.security.adminRestrictionsTitle": "Admin Restrictions", + "mgmt.config.security.encryption.authPubKeyTitle": "Authentication Public Key", + "mgmt.config.security.encryption.enryptionInfo": "For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.", + "mgmt.config.security.encryption.genMissingAuthCheckbox.genMissingAuthLabel": "Generate missing Authentication Certificates automatically (Default: true)", + "mgmt.config.security.encryption.httpsCertSelection.httpsCertLabel": "HTTPS Certificates", + "mgmt.config.security.encryption.httpsCertSelection.sslDisabledChoice": "Do not use HTTPS (very unsecure)", + "mgmt.config.security.encryption.httpsCertSelection.sslLetsEncryptChoice": "Automatically generate certificates using Let's Encrypt (Recommended)", + "mgmt.config.security.encryption.httpsCertSelection.sslProvidedChoice": "I have my own certificates", + "mgmt.config.security.encryption.httpsCertSelection.sslSelfSignedChoice": "Locally self-sign certificates (unsecure)", + "mgmt.config.security.encryption.overwriteWildcardInput.overwriteWildcardLabel": "(optional, only if you know what you are doing) Override Wildcard Domains (comma separated, need to add both wildcard AND root domain like in the placeholder)", + "mgmt.config.security.encryption.rootHttpsPubKeyTitle": "Root HTTPS Public Key", + "mgmt.config.security.encryption.sslCertForceRenewCheckbox.sslCertForceRenewLabel": "Force HTTPS Certificate Renewal On Next Save", + "mgmt.config.security.encryption.sslLetsEncryptDnsSelection.sslLetsEncryptDnsLabel": "Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)", + "mgmt.config.security.encryption.sslLetsEncryptEmailInput.sslLetsEncryptEmailLabel": "Email address for Let's Encrypt", + "mgmt.config.security.encryption.wildcardCheckbox.wildcardLabel": "Use Wildcard Certificate for the root domain of ", + "mgmt.config.security.encryptionTitle": "Encryption", + "mgmt.config.security.geoBlockSelection": "Choose which countries you want to {{blockAllow}}", + "mgmt.config.security.geoBlockSelection.geoBlockLabel": "Geo-Blocking: (Those countries will be {{blockAllow}})", + "mgmt.config.security.geoBlockSelection.geoBlockLabel.varAllow": "allowed to access", + "mgmt.config.security.geoBlockSelection.geoBlockLabel.varBlock": "blocked from accessing", + "mgmt.config.security.geoBlockSelection.varAllow": "allow", + "mgmt.config.security.geoBlockSelection.varBlock": "block", + "mgmt.config.security.geoblock.resetToDefaultButton": "Reset to default (most dangerous countries)", + "mgmt.config.security.invertBlacklistCheckbox.invertBlacklistLabel": "Use list as whitelist instead of blacklist", + "mgmt.constellation.dns.resetButton": "Reset", + "mgmt.constellation.dnsBlocklistsTitle": "DNS Blocklists", + "mgmt.constellation.dnsTitle": "Constellation Internal DNS", + "mgmt.constellation.externalText": "You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.", + "mgmt.constellation.isRelay.label": "Can Relay Traffic", + "mgmt.constellation.resetLabel": "Reset Network", + "mgmt.constellation.resetText": "This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone.", + "mgmt.constellation.restartButton": "Restart VPN Service", + "mgmt.constellation.setup.addDeviceSuccess": "Device added successfully! Download scan the QR Code from the Cosmos app or download the relevant files to your device along side the config and network certificate to connect:", + "mgmt.constellation.setup.addDeviceText": "Add a Device to the constellation using either the Cosmos or Nebula client", + "mgmt.constellation.setup.addDeviceTitle": "Add Device", + "mgmt.constellation.setup.deviceName.label": "Device Name", + "mgmt.constellation.setup.dns.customEntries": "DNS Custom Entries", + "mgmt.constellation.setup.dns.resetDefault": "Reset Default", + "mgmt.constellation.setup.dnsBlocklistText": "Use Blacklists to block domains", + "mgmt.constellation.setup.dnsBlocklistUrls.label": "DNS Blocklist URLs", + "mgmt.constellation.setup.dnsExpiryWarning": "When changing your DNS records, always use private mode on your browser and allow some times for various caches to expire.", + "mgmt.constellation.setup.dnsText": "This is a DNS that runs inside your Constellation network. It automatically rewrites your domains DNS entries to be local to your network, and also allows you to do things like block ads and trackers on all devices connected to your network. You can also add custom DNS entries to resolve to specific IP addresses. This DNS server is only accessible from inside your network.", + "mgmt.constellation.setup.enabledCheckbox": "Constellation Enabled", + "mgmt.constellation.setup.externalConfig.label": "Upload External Constellation Network File", + "mgmt.constellation.setup.firewallInfo": "Coming soon. This feature will allow you to open and close ports individually on each device and decide who can access them.", + "mgmt.constellation.setup.hostnameInfo": "This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. If you change this value, you will need to reset your network and reconnect all the clients!", + "mgmt.constellation.setup.ip.label": "Constellation IP Address", + "mgmt.constellation.setup.ipTitle": "Constellation IP", + "mgmt.constellation.setup.owner.label": "Owner", + "mgmt.constellation.setup.privNode.label": "This node is Private (no public IP)", + "mgmt.constellation.setup.pubHostname.label": "Public Hostname", + "mgmt.constellation.setup.pubKey.label": "Public Key (Optional)", + "mgmt.constellation.setup.relayRequests.label": "Relay requests via this Node", + "mgmt.constellation.setup.unsafeRoutesText": "Coming soon. This feature will allow you to tunnel your traffic through your devices to things outside of your constellation.", + "mgmt.constellation.setup.unsafeRoutesTitle": "Unsafe Routes", + "mgmt.constellation.setupText": "Constellation is a VPN that runs inside your Cosmos network. It automatically connects all your devices together, and allows you to access them from anywhere. Please refer to the <0>documentation for more information. In order to connect, please use the <1>Constellation App. Constellation is currently free to use until the end of the beta, planned January 2024.", + "mgmt.constellation.setupTitle": "Constellation Setup", + "mgmt.constellation.setuplighthouseTitle": "Lighthouse Setup", + "mgmt.constellation.showConfigButton": "Show VPN Config", + "mgmt.constellation.showLogsButton": "Show VPN logs", + "mgmt.cron.editCron.customText": "Create a custom job to run a shell command in a container. Leave the container field empty to run on the host", + "mgmt.cron.editCron.customText.onHostOnly": "Running on the host only works if Cosmos is not itself running in a container", + "mgmt.cron.editCronTitle": "Edit Job", + "mgmt.cron.invalidCron": "Invalid CRONTAB format (use 6 parts)", + "mgmt.cron.list.state.lastRan": "Last run", + "mgmt.cron.list.state.running": "Running - Started", + "mgmt.cron.newCron.commandInput.commandLabel": "Command to run (ex. echo 'Hello world')", + "mgmt.cron.newCron.cronNameInput.cronNameLabel": "Name of the job", + "mgmt.cron.newCron.crontabInput.crontabLabel": "Schedule (using crontab syntax)", + "mgmt.cron.newCron.submitButton": "Submit", + "mgmt.cron.newCronTitle": "New Job", + "mgmt.monitoring.alerts.actionTriggersTitle": "Action Triggers", + "mgmt.monitoring.alerts.addActionButton": "Add Action", + "mgmt.openId.experimentalWarning": "This is an experimental feature. It is recommended to use with caution. Please report any issue you find!", + "mgmt.openId.newSecret": "New Secret", + "mgmt.openId.redirect": "Redirect", + "mgmt.openId.redirectUri": "Redirect URI", + "mgmt.openId.resetSecret": "Reset Secret", + "mgmt.openId.secretUpdated": "Secret has been updated. Please copy it now as it will not be shown again.", + "mgmt.openid.newClientTitle": "New client", + "mgmt.openid.newMfa": "New MFA Setup", + "mgmt.openid.newMfa.enterOtp": "Enter your OTP", + "mgmt.openid.newMfa.otpEnterTokenText": "Once you have scanned the QR code or entered the code manually, enter the token from your authenticator app below", + "mgmt.openid.newMfa.otpManualCode": "...Or enter this code manually in it", + "mgmt.openid.newMfa.otpManualCode.showButton": "Show manual code", + "mgmt.openid.newMfa.requires2faText": "This server requires 2FA. Scan this QR code with your <1>authenticator app to proceed", + "mgmt.openid.newMfa.tokenRequiredValidation": "Token is required", + "mgmt.openid.newMfa.tokenmax6charValidation": "Token must be at most 6 characters", + "mgmt.openid.newMfa.tokenmin6charValidation": "Token must be at least 6 characters", + "mgmt.openid.newMfa.wrongOtpValidation": "Wrong OTP. Try again", + "mgmt.scheduler.customJobsTitle": "Custom Jobs", + "mgmt.scheduler.lastLogs": "Last logs for", + "mgmt.scheduler.list.action.logs": "Logs", + "mgmt.scheduler.list.action.run": "Run", + "mgmt.scheduler.list.scheduleTitle": "Schedule", + "mgmt.scheduler.list.status.lastRunExitedOn": "Last run exited with an error on", + "mgmt.scheduler.list.status.lastRunFinishedOn": "Last run finished on", + "mgmt.scheduler.list.status.lastRunFinishedOn.duration": "Duration", + "mgmt.scheduler.list.status.neverRan": "Never ran", + "mgmt.scheduler.list.status.runningSince": "Running Since ", + "mgmt.scheduler.oneTimeJobsTitle": "One Time Jobs", + "mgmt.scheduler.parityDiskJobsTitle": "Parity Disks Jobs", + "mgmt.servApp.container.urls.exposeText": "Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL.", + "mgmt.servApp.container.urls.exposeTitle": "Expose ServApp", + "mgmt.servApp.exposeDesc": "Expose containerName to the internet", + "mgmt.servApp.newContainer.reviewStartButton": "Review & Start", + "mgmt.servApp.newServAppButton": "Start New Servapp", + "mgmt.servApp.url": "Create a URL to access this ServApp", + "mgmt.servApps.autoUpdateCheckbox": "Auto Update Container", + "mgmt.servApps.container.delete.cronjob": "Cron Job", + "mgmt.servApps.container.delete.done": "Done", + "mgmt.servApps.container.delete.route": "Route", + "mgmt.servApps.container.deleteService": "Delete Service", + "mgmt.servApps.container.deleteServiceStatus": "Deletion status:", + "mgmt.servApps.container.network.linkContainerButton": "Link Container", + "mgmt.servApps.container.network.linkContainerTitle": "Link with container", + "mgmt.servApps.container.overview.healthTitle": "Health", + "mgmt.servApps.container.overview.imageTitle": "Image", + "mgmt.servApps.container.overview.ipAddressTitle": "IP Address", + "mgmt.servApps.container.overview.settingsTitle": "Settings", + "mgmt.servApps.container.protocols.errorOnlyCheckbox": "Error Only", + "mgmt.servApps.container.selectWhatToDelete": "Select what you wish to delete:", + "mgmt.servApps.createNetwork.parentReqForMacvlan": "Parent interface is required for MACVLAN", + "mgmt.servApps.createdChip.createdLabel": "\"Created\"", + "mgmt.servApps.deadChip.deadLabel": "Dead", + "mgmt.servApps.driver.none": "mgmt.servApps.driver.none", + "mgmt.servApps.exitedChip.exitedLabel": "Exited", + "mgmt.servApps.exportDockerBackupButton.exportDockerBackupLabel": "Export Docker Backup", + "mgmt.servApps.networks.containerPortInput.containerPortLabel": "Container Port", + "mgmt.servApps.networks.containerotRunningWarning": "This container is not running. Editing any settings will cause the container to start again.", + "mgmt.servApps.networks.exposePortsTitle": "Expose Ports", + "mgmt.servApps.networks.forcedSecurityWarning": "This container is forced to be secured. You cannot expose any ports to the internet directly, please create a URL in Cosmos instead. You also cannot connect it to the Bridge network.", + "mgmt.servApps.networks.modeInput.modeLabel": "Network Mode", + "mgmt.servApps.networks.removedNetConnectedDisconnect": "Disconnect It", + "mgmt.servApps.networks.removedNetConnectedEitherRecreate": "Either re-create it or", + "mgmt.servApps.networks.removedNetConnectedError": "You are connected to a network that has been removed:", + "mgmt.servApps.networks.updatePortsButton": "Update Ports", + "mgmt.servApps.newChip.newLabel": "New", + "mgmt.servApps.newContainer.chooseUrl": "Choose URL for", + "mgmt.servApps.newContainer.cosmosOutdatedError": "This service requires a newer version of Cosmos. Please update Cosmos to install this service.", + "mgmt.servApps.newContainer.customize": "Customize ", + "mgmt.servApps.newContainer.customize2": " ", + "mgmt.servApps.newContainer.networkSettingsTitle": "Network Settings", + "mgmt.servApps.newContainer.serviceNameInput": "Choose your service name", + "mgmt.servApps.notRunningWarning": "This container is not running. Editing any settings will cause the container to start again.", + "mgmt.servApps.pausedChip.pausedLabel": "Paused", + "mgmt.servApps.removingChip.removingLabel": "Removing", + "mgmt.servApps.restartingChip.restartingLabel": "Restarting", + "mgmt.servApps.runningChip.runningLabel": "Running", + "mgmt.servApps.startToEditInfo": "Start container to edit", + "mgmt.servApps.volumes.containerNotRunningWarning": "This container is not running. Editing any settings will cause the container to start again.", + "mgmt.servApps.volumes.newVolume.driverSelection.localChoice": "Local", + "mgmt.servApps.volumes.newVolumeTitle": "New Volume", + "mgmt.servapps.actionBar.kill": "Kill", + "mgmt.servapps.actionBar.noUpdate": "No Update Available. Click to Force Pull", + "mgmt.servapps.actionBar.pause": "Pause", + "mgmt.servapps.actionBar.recreate": "Re-create", + "mgmt.servapps.actionBar.restart": "Restart", + "mgmt.servapps.actionBar.start": "Start", + "mgmt.servapps.actionBar.stop": "Stop", + "mgmt.servapps.actionBar.unpause": "Unpause", + "mgmt.servapps.actionBar.update": "Update Available", + "mgmt.servapps.actionBar.updating": "Updating ServApp...", + "mgmt.servapps.compose": "Compose", + "mgmt.servapps.compose.installButton": "Install", + "mgmt.servapps.compose.installTitle": "Installation", + "mgmt.servapps.container.compose.createServiceButton": "Create Service - Preview", + "mgmt.servapps.container.compose.createServiceSuccess": "Service Created!", + "mgmt.servapps.container.compose.editServiceTitle": "Edit Service", + "mgmt.servapps.containers.terminal.connectButton": "Connect", + "mgmt.servapps.containers.terminal.connectedToText": "Connected to ", + "mgmt.servapps.containers.terminal.disconnectButton": "Disconnect", + "mgmt.servapps.containers.terminal.disconnectedFromText": "Disconnected from ", + "mgmt.servapps.containers.terminal.mainprocessTty": "main process TTY", + "mgmt.servapps.containers.terminal.newShellButton": "New Shell", + "mgmt.servapps.containers.terminal.terminalNotInteractiveWarning": "This container is not interactive. If you want to connect to the main process, ", + "mgmt.servapps.containers.terminal.ttyEnableButton": "Enable TTY", + "mgmt.servapps.importComposeFileButton": "Import Compose File", + "mgmt.servapps.networks.attackNetwork": "Attach to Cosmos", + "mgmt.servapps.networks.containers": "Containers", + "mgmt.servapps.networks.list.bridge": "Bridge", + "mgmt.servapps.networks.list.host": "Host", + "mgmt.servapps.networks.list.macvlan": "MACVLAN", + "mgmt.servapps.networks.list.networkIpam": "IPAM gateway / mask", + "mgmt.servapps.networks.list.networkName": "Network Name", + "mgmt.servapps.networks.list.networkNoIp": "No IP", + "mgmt.servapps.networks.list.networkproperties": "Properties", + "mgmt.servapps.networks.list.newNetwork": "New Network", + "mgmt.servapps.networks.list.overlay": "Overlay", + "mgmt.servapps.networks.list.parentIf": "Parent Interface", + "mgmt.servapps.networks.list.subnet": "Subnet (optional)", + "mgmt.servapps.networks.volumes": "Volumes", + "mgmt.servapps.newContainer.devices.containerPathInput.containerPathLabel": "Container Path", + "mgmt.servapps.newContainer.devices.hostPathInput.hostPathLabel": "Host Path", + "mgmt.servapps.newContainer.devicesTitle": "Devices", + "mgmt.servapps.newContainer.env.envKeyInput.envKeyLabel": "Key", + "mgmt.servapps.newContainer.env.envValueInput.envValueLabel": "Value", + "mgmt.servapps.newContainer.env.keyNotUniqueError": "Environment Variables must be unique", + "mgmt.servapps.newContainer.envTitle": "Environment Variables", + "mgmt.servapps.newContainer.forceSecureCheckbox.forceSecureLabel": "Force secure container", + "mgmt.servapps.newContainer.imageUpdateWarning": "You have updated the image. Clicking the button below will pull the new image, and then only can you update the container.", + "mgmt.servapps.newContainer.interactiveCheckbox.interactiveLabel": "Interactive Mode", + "mgmt.servapps.newContainer.label.labelNotUniqueError": "Labels must be unique", + "mgmt.servapps.newContainer.labelsTitle": "Labels", + "mgmt.servapps.newContainer.pullImageButton": "Pull New Image", + "mgmt.servapps.newContainer.pullingImageStatus": "Pulling New Image...", + "mgmt.servapps.newContainer.restartPolicyInput.restartPolicyLabel": "Restart Policy", + "mgmt.servapps.newContainer.restartPolicyInput.restartPolicyPlaceholder": "Restart Policy", + "mgmt.servapps.newContainer.updateContainerButton": "Update Container", + "mgmt.servapps.newContainer.volumes.bindInput": "Bind", + "mgmt.servapps.newContainer.volumes.mountNotUniqueError": "Mounts must have unique targets", + "mgmt.servapps.newContainer.volumes.newMountButton": "New Mount Point", + "mgmt.servapps.newContainer.volumes.updateVolumesButton": "Update Volumes", + "mgmt.servapps.newContainer.volumesTitle": "Volume Mounts", + "mgmt.servapps.newContainerTitle": "Docker Container Setup", + "mgmt.servapps.overview": "Overview", + "mgmt.servapps.pasteComposeButton.pasteComposePlaceholder": "Paste your docker-compose.yml / cosmos-compose.json here or use the file upload button.", + "mgmt.servapps.routeConfig.routeNotFound": "Route not found", + "mgmt.servapps.routeConfig.setup": "Setup", + "mgmt.servapps.terminal": "Terminal", + "mgmt.servapps.updatesAvailableFor": "Update are available for", + "mgmt.servapps.viewDetailsButton": "View Details", + "mgmt.servapps.viewStackButton": "View Stack", + "mgmt.servapps.volumes.list.ScopeTitle": "Scope", + "mgmt.servapps.volumes.volumeName": "Volume Name", + "mgmt.storage.available": "available", + "mgmt.storage.chown": "Change mount folder owner (optional, ex. 1000:1000)", + "mgmt.storage.configName.configNameLabel": "Config Name", + "mgmt.storage.confirmParityDeletion": "Are you sure you want to delete this parity?", + "mgmt.storage.confirmPwd.confirmPwdLabel": "Confirm Your Password", + "mgmt.storage.dataDisksTitle": "Data Disks", + "mgmt.storage.deviceTitle": "Device", + "mgmt.storage.diskformatTitle": "Disk Format", + "mgmt.storage.disks": "Disks", + "mgmt.storage.externalStorage": "External Storage", + "mgmt.storage.externalStorageText": "Coming soon. This feature will allow you to mount external cloud (Dropbox, Onedrive, ...) to your server.", + "mgmt.storage.formatButton": "Format", + "mgmt.storage.formatDiskTitle": "Format Disk", + "mgmt.storage.formattingLog": "Formatting", + "mgmt.storage.list.fixText": "Fix", + "mgmt.storage.list.scrubText": "Scrub", + "mgmt.storage.list.syncText": "Sync", + "mgmt.storage.merge.fsOptions.fsOptionsLabel": "Addional mergerFS options (optional, comma separated)", + "mgmt.storage.mergeButton": "Merge", + "mgmt.storage.mergeText": "You are about to merge disks together. This operation is safe and reversible. It will not affect the data on the disks, but will make the content available to be viewed in the file explorer as a single disk.", + "mgmt.storage.mergeTitle": "Merge Disks", + "mgmt.storage.mount.permanent": "Permanent", + "mgmt.storage.mount.whatToMountLabel": "What to mount", + "mgmt.storage.mountPath": "Path to mount to", + "mgmt.storage.mountPicker": "Select Targets", + "mgmt.storage.mountedAtText": "Mounted At", + "mgmt.storage.mounts": "Mounts", + "mgmt.storage.newMerge.newMergeButton": "Create Merge", + "mgmt.storage.newMount.newMountButton": "New Mount", + "mgmt.storage.optionsTitle": "Options", + "mgmt.storage.parityDisksTitle": "Parity Disks", + "mgmt.storage.parityTitle": "Parity", + "mgmt.storage.pathPrefixMntValidation": "Path should start with /mnt/ or /var/mnt", + "mgmt.storage.pathTitle": "Path", + "mgmt.storage.raidText": "Coming soon. This feature will allow you to create RAID arrays with your disks.", + "mgmt.storage.raidTitle": "RAID", + "mgmt.storage.runningInsideContainerWarning": "You are running Cosmos inside a Docker container. As such, it will only have limited access to your disks and their informations.", + "mgmt.storage.selectMin2": "Select at least 2 disks", + "mgmt.storage.sharesText": "Coming soon. This feature will allow you to share folders with different protocols (SMB, FTP, ...)", + "mgmt.storage.sharesTitle": "Shares", + "mgmt.storage.smart.for": "S.M.A.R.T. for", + "mgmt.storage.smart.health": "Health", + "mgmt.storage.smart.noSmartError": "No S.M.A.R.T. data available for this disk. If you are running Cosmos behind some sort of virtualization or containerization, it is probably the reason why the data is not available.", + "mgmt.storage.smart.threshholdTooltip": "This value is a % of health (100 is best). Next to them is a threshold below which it is urgent to replace your hardrive.", + "mgmt.storage.snapraid.addDatadisk": "Add Data Disk", + "mgmt.storage.snapraid.createParity.Step1Text": "First, select the parity disk(s). One parity disk will protect against one disk failure, two parity disks will protect against two disk failures, and so on. Remember that those disks will be used only for parity, and will not be available for data storage. Parity disks must be at least as large as the largest data disk, and should be empty.", + "mgmt.storage.snapraid.createParity.Step2Text": "Select the data disks you want to protect with the parity disk(s).", + "mgmt.storage.snapraid.createParity.Step3Text": "Set the sync and scrub intervals. The sync interval is the time at which parity is updated. The scrub interval is the time at which the parity is checked for errors. This is using the CRONTAB syntax with seconds.", + "mgmt.storage.snapraid.createParity.newDisks": "New Parity Disks", + "mgmt.storage.snapraid.createParity.step": "Step", + "mgmt.storage.snapraid.createParityDisksButton": "Create Parity Disks", + "mgmt.storage.snapraid.createParityInfo": "You are about to create parity disks. This operation is safe and reversible. Parity disks are used to protect your data from disk failure. When creating a parity disk, the data disks you want to protect. Do not add a disk containing the system or another parity disk.", + "mgmt.storage.snapraid.min1parity": "Select at least 1 parity disk", + "mgmt.storage.snapraid.min2datadisks": "Select at least 2 data disk", + "mgmt.storage.snapraid.min3chars": "Name should be at least 3 characters", + "mgmt.storage.snapraid.notAlphanumeric": "Name should be alphanumeric", + "mgmt.storage.snapraid.removeDatadisk": "Remove Data Disk", + "mgmt.storage.snapraid.scrubInterval.scrubIntervalLabel": "Scrub Interval", + "mgmt.storage.snapraid.storageParity": "Storage Parity", + "mgmt.storage.snapraid.syncInterval.syncIntervalLabel": "Sync Interval", + "mgmt.storage.startFormatLog": "Starting format disk {{disk}}...", + "mgmt.storage.syncScrubIntervalTitle": "Sync/Scrub Intervals", + "mgmt.storage.typeTitle": "Type", + "mgmt.storage.unMountDiskButton": "{{unMount}} disk", + "mgmt.storage.unMountDiskText": "You are about to {{unMount}} the disk {{disk}}{{mountpoint}}. This will make the content {{unAvailable}} to be viewed in the file explorer. Permanent {{unMount}} will persist after reboot.", + "mgmt.storage.unMountText": "You are about to {{unMount}} a folder {{mountpoint}}. This will make the content {{unAvailable}} to be viewed in the file explorer. Permanent {{unMount}} will persist after reboot.", + "mgmt.storage.unavailable": "unavailable", + "mgmt.urls.edit.advancedSettings.advancedSettingsInfo": "These settings are for advanced users only. Please do not change these unless you know what you are doing.", + "mgmt.urls.edit.advancedSettings.filterIpWarning": "This setting will filter out all requests that do not come from the specified IPs. This requires your setup to report the true IP of the client. By default it will, but some exotic setup (like installing docker/Cosmos on Windows, or behind Cloudlfare) will prevent Cosmos from knowing what is the client's real IP. If you used \"Restrict to Constellation\" above, Constellation IPs will always be allowed regardless of this setting.", + "mgmt.urls.edit.advancedSettings.hideFromDashboardCheckbox.hideFromDashboardLabel": "Hide from Dashboard", + "mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderLabel": "Overwrite Host Header (use this to chain resolve request from another server/ip)", + "mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderPlaceholder": "Overwrite Host Header", + "mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpLabel": "Whitelist Inbound IPs and/or IP ranges (comma separated)", + "mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpPlaceholder": "Whitelist Inbound IPs and/or IP ranges (comma separated)", + "mgmt.urls.edit.advancedSettingsTitle": "Advanced Settings", + "mgmt.urls.edit.basicSecurity.authEnabledCheckbox.authEnabledLabel": "Authentication Required", + "mgmt.urls.edit.basicSecurity.restrictToConstellationCheckbox.restrictToConstellationLabel": "Restrict access to Constellation VPN", + "mgmt.urls.edit.basicSecurity.smartShieldEnabledCheckbox.smartShieldEnabledLabel": "Smart Shield Protection", + "mgmt.urls.edit.basicSecurityTitle": "Basic Security", + "mgmt.urls.edit.insecureHttpsCheckbox.insecureHttpsLabel": "Accept Insecure HTTPS Target (not recommended)", + "mgmt.urls.edit.newUrlTitle": "new URL", + "mgmt.urls.edit.pathPrefixInputx.pathPrefixLabel": "Path Prefix", + "mgmt.urls.edit.pathPrefixInputx.pathPrefixPlaceholder": "Path Prefix", + "mgmt.urls.edit.sourceInfo": "What URL do you want to access your target from?", + "mgmt.urls.edit.stripPathCheckbox.stripPathLabel": "Strip Path Prefix", + "mgmt.urls.edit.targetFolderPathInput.targetFolderPathLabel": "Target Folder Path", + "mgmt.urls.edit.targetSettings.targetUrlInput.targetUrlLabel": "Target URL", + "mgmt.urls.edit.targetSettingsTitle": "Target Settings", + "mgmt.urls.edit.targetType.modeSelection.modeLabel": "Mode", + "mgmt.urls.edit.targetType.modeSelection.proxyChoice": "Proxy", + "mgmt.urls.edit.targetType.modeSelection.redirectChoice": "Redirection", + "mgmt.urls.edit.targetType.modeSelection.servAppChoice": "ServApp - Docker Container", + "mgmt.urls.edit.targetType.modeSelection.spaChoice": "Single Page Application", + "mgmt.urls.edit.targetType.modeSelection.staticChoice": "Static Folder", + "mgmt.urls.edit.targetTypeInfo": "What are you trying to access with this route?", + "mgmt.urls.edit.targetTypeTitle": "Target Type", + "mgmt.urls.edit.useHostCheckbox.useHostLabel": "Use Host", + "mgmt.urls.edit.usePathPrefixCheckbox.usePathPrefixLabel": "Use Path Prefix", + "mgmt.usermgmt.adminLabel": "Admin", + "mgmt.usermgmt.createUser.emailOptInput.emailOptLabel": "Email Adress (Optional)", + "mgmt.usermgmt.createUserTitle": "Create User", + "mgmt.usermgmt.deleteUserConfirm": "Are you sure you want to delete user", + "mgmt.usermgmt.deleteUserTitle": "Delete User", + "mgmt.usermgmt.editEmail.emailInput.emailLabel": "Email Adress", + "mgmt.usermgmt.editEmailText": "Use this form to invite edit {{user}}'s Email.", + "mgmt.usermgmt.editEmailTitle": "Edit Email", + "mgmt.usermgmt.inviteExpiredLabel": "Invite Expired", + "mgmt.usermgmt.invitePendingLabel": "Invite Pending", + "mgmt.usermgmt.inviteUser.emailSentAltShare": "Send this link to", + "mgmt.usermgmt.inviteUser.emailSentAltShareLink": "Alternatively you can also share the link below:", + "mgmt.usermgmt.inviteUser.emailSentAltShareTo": "to", + "mgmt.usermgmt.inviteUser.emailSentConfirmation": "An email has been sent", + "mgmt.usermgmt.inviteUser.emailSentwithLink": "with a link to", + "mgmt.usermgmt.inviteUserText": "Use this form to invite a new user to the system.", + "mgmt.usermgmt.inviteUserTitle": "Invite User", + "mgmt.usermgmt.lastLogin": "Last Login", + "mgmt.usermgmt.reset2faButton": "Reset 2FA", + "mgmt.usermgmt.sendPasswordResetButton": "Send password reset", + "navigation.home.Avx": "AVX Supported", + "navigation.home.LetsEncryptEmailError": "You have enabled Let's Encrypt for automatic HTTPS Certificate. You need to provide the configuration with an email address to use for Let's Encrypt in the configs.", + "navigation.home.LetsEncryptError": "There are errors with your Let's Encrypt configuration or one of your routes, please fix them as soon as possible:", + "navigation.home.availRam": "avail.", + "navigation.home.configChangeRequiresRestartError": "You have made changes to the configuration that require a restart to take effect. Please restart Cosmos to apply the changes.", + "navigation.home.cosmosNotDockerHostError": "Your Cosmos server is not running in the docker host network mode. It is recommended that you migrate your install.", + "navigation.home.dbCantConnectError": "Database cannot connect, this will impact multiple feature of Cosmos. Please fix ASAP!", + "navigation.home.localhostnotRecommendedError": "You are using localhost or 0.0.0.0 as a hostname in the configuration. It is recommended that you use a domain name or an IP instead.", + "navigation.home.network": "NETWORK", + "navigation.home.newCosmosVersionError": "A new version of Cosmos is available! Please update to the latest version to get the latest features and bug fixes.", + "navigation.home.noApps": "You have no apps configured. Please add some apps in the configuration panel.", + "navigation.home.noAppsTitle": "No Apps", + "navigation.home.noAvx": "No AVX Support", + "navigation.home.rcvNet": "rcv", + "navigation.home.trsNet": "trs", + "navigation.home.usedRam": "used", + "navigation.market.applicationsTitle": "Applications", + "navigation.market.compose": "compose", + "navigation.market.filterDuplicateCheckbox": "Filter Duplicates", + "navigation.market.image": "image", + "navigation.market.newSources.additionalMarketsInfo": "This allows you to add additional 3rd party repos to the App-Store.", + "navigation.market.newSources.additionalMarketsInfo.href": "start here", + "navigation.market.newSources.additionalMarketsInfo.moreInfo": "To find new sources,", + "navigation.market.repository": "repository", + "navigation.market.search": "Search {{count}} Apps...", + "navigation.market.sources.addSourceButton": "Add Source", + "navigation.market.sources.editSourcesButton": "Sources", + "navigation.market.sources.nameNotUniqueValidation": "Name must be unique", + "navigation.market.sources.urlRequiredValidation": "URL is required", + "navigation.market.sourcesTitle": "Edit Sources", + "navigation.market.startServAppButton": "Start ServApp", + "navigation.market.unofficialMarketTooltip": "This app is not hosted on the Cosmos Cloud App Store. It is not officially verified and tested.", + "navigation.market.viewButton": "View", + "navigation.monitoring.alerts.action.edit": "Edit Alert", + "navigation.monitoring.alerts.action.edit.actionTypeInput.actionTypeLabel": "Action Type", + "navigation.monitoring.alerts.action.edit.conditionOperator.validation": "Condition operator is required", + "navigation.monitoring.alerts.action.edit.conditionValue.validation": "Condition value is required", + "navigation.monitoring.alerts.action.edit.period.validation": "Period is required", + "navigation.monitoring.alerts.action.edit.severitySelection.severityLabel": "Severity", + "navigation.monitoring.alerts.action.edit.trackingMetric.validation": "Tracking metric is required", + "navigation.monitoring.alerts.actions.restart": "Restart container causing the alert", + "navigation.monitoring.alerts.actions.restartActionInfo": "Restart action will attempt to restart any Containers attachted to the metric. This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU", + "navigation.monitoring.alerts.actions.sendEmail": "Send an Email", + "navigation.monitoring.alerts.actions.sendNotification": "Send a notification", + "navigation.monitoring.alerts.actions.stop": "Stop/Disable resources causing the alert", + "navigation.monitoring.alerts.actions.stopActionInfo": "Stop action will attempt to stop/disable any resources (ex. Containers, routes, etc... ) attachted to the metric. This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU", + "navigation.monitoring.alerts.actionsTitle": "Actions", + "navigation.monitoring.alerts.alertNameLabel": "Name of the alert", + "navigation.monitoring.alerts.astTriggeredTitle": "Last Triggered", + "navigation.monitoring.alerts.conditionLabel": "Condition is a percent of max value", + "navigation.monitoring.alerts.conditionOperatorLabel": "Trigger Condition Operator", + "navigation.monitoring.alerts.conditionTitle": "Condition", + "navigation.monitoring.alerts.conditionValueLabel": "Trigger Condition Value", + "navigation.monitoring.alerts.newAlertButton": "New Alert", + "navigation.monitoring.alerts.periodLabel": "Period (how often to check the metric)", + "navigation.monitoring.alerts.periodTitle": "Period", + "navigation.monitoring.alerts.resetToDefaultButton": "Reset to default", + "navigation.monitoring.alerts.throttleCheckbox.throttleLabel": "Throttle (only triggers a maximum of once a day)", + "navigation.monitoring.alerts.trackingMetricLabel": "Metric to track", + "navigation.monitoring.alerts.trackingMetricTitle": "Tracking Metric", + "navigation.monitoring.alertsTitle": "Alerts", + "navigation.monitoring.daily": "Daily", + "navigation.monitoring.events.datePicker.fromLabel": "From", + "navigation.monitoring.events.datePicker.toLabel": "To", + "navigation.monitoring.events.eventsFound": "{{count}} event found from {{from}} to {{to}}", + "navigation.monitoring.events.eventsFound_other": "{{count}} events found from {{from}} to {{to}}", + "navigation.monitoring.events.eventsFound_zero": "No events found from {{from}} to {{to}}", + "navigation.monitoring.events.loadMoreButton": "Load more", + "navigation.monitoring.events.searchInput.searchPlaceholder": "Search (text or bson)", + "navigation.monitoring.eventsTitle": "Events", + "navigation.monitoring.hourly": "Hourly", + "navigation.monitoring.latest": "Latest", + "navigation.monitoring.proxyTitle": "Proxy", + "navigation.monitoring.resourceDashboard.averageNetworkTitle": "Containers - Average Network", + "navigation.monitoring.resourceDashboard.averageResourcesTitle": "Containers - Average Resources", + "navigation.monitoring.resourceDashboard.blockReasonTitle": "Reasons For Blocked Requests", + "navigation.monitoring.resourceDashboard.blockedRequestsTitle": "Blocked Requests", + "navigation.monitoring.resourceDashboard.diskUsageTitle": "Disk Usage", + "navigation.monitoring.resourceDashboard.reasonByBots": "Bots", + "navigation.monitoring.resourceDashboard.reasonByGeo": "By Geolocation (blocked countries)", + "navigation.monitoring.resourceDashboard.reasonByHostname": "By Hostname (usually IP scanning threat)", + "navigation.monitoring.resourceDashboard.reasonByRef": "By Referer", + "navigation.monitoring.resourceDashboard.reasonBySmartShield": "Smart Shield (various abuse metrics such as time, size, brute-force, concurrent requests, etc...). It does not include blocking for banned IP to save resources in case of potential attacks", + "navigation.monitoring.resourceDashboard.reasonByWhitelist": "By IP Whitelists (Including restricted to Constellation)", + "navigation.monitoring.resourceDashboard.requestsPerUrlTitle": "Requests Per URLs", + "navigation.monitoring.resourceDashboard.requestsTitle": "Requests Resources", + "navigation.monitoring.resourceDashboard.responsesTitle": "Requests Responses", + "navigation.monitoring.resourcesTitle": "Resources", + "navigation.monitoringTitle": "Server Monitoring", + "newInstall.LetsEncrypt.cloudflareWarning": "If you are using Cloudflare, make sure the DNS record is NOT set to Proxied (you should not see the orange cloud but a grey one). Otherwise Cloudflare will not allow Let's Encrypt to verify your domain.
Alternatively, you can also use the DNS challenge.", + "newInstall.LetsEncrypt.dnsChallengeInfo": "You have enabled the DNS challenge. Make sure you have set the environment variables for your DNS provider. You can enable it now, but make sure you have set up your API tokens accordingly before attempting to access Cosmos after this installer. See doc here: <1>https://go-acme.github.io/lego/dns/", + "newInstall.adminAccountText": "Create a local admin account to manage your server. Email is optional and used for notifications and password recovery.", + "newInstall.adminAccountTitle": "Admin Account 🔑 (step 4/4)", + "newInstall.applyRestartAction": "Apply and Restart", + "newInstall.checkInputValidation": "Please check you have filled all the inputs properly", + "newInstall.cleanInstallCheckbox": "Clean install (remove any existing config files)", + "newInstall.dbConnected": "Database is connected.", + "newInstall.dbInstalling": "Installing Database...", + "newInstall.dbNotConnected": "Database is not connected!", + "newInstall.dbSelection.createChoice": "Automatically create a secure database (recommended)", + "newInstall.dbSelection.dbLabel": "Select your choice", + "newInstall.dbSelection.disabledChoice": "Disable User Management and UI", + "newInstall.dbSelection.providedChoice": "Supply my own database credentials", + "newInstall.dbText": "Cosmos is using a MongoDB database to store all the data. It is optional, but Authentication as well as the UI will not work without a database.", + "newInstall.dbTitle": "Database 🗄️ (step 2/4)", + "newInstall.dbUrlInput.dbUrlLabel": "Database URL", + "newInstall.dockerAvail": "Docker is installed and running.", + "newInstall.dockerChecking": "Rechecking Docker Status...", + "newInstall.dockerNotConnected": "Docker is not connected! Please check your docker connection.
Did you forget to add
-v /var/run/docker.sock:/var/run/docker.sock
to your docker run command?
if your docker daemon is running somewhere else, please add
-e DOCKER_HOST=...
to your docker run command.", + "newInstall.dockerTitle": "Docker 🐋 (step 1/4)", + "newInstall.finishText": "Well done! You have successfully installed Cosmos. You can now login to your server using the admin account you created. If you have changed the hostname, don't forget to use that URL to access your server after the restart. If you have are running into issues, check the logs for any error messages and edit the file in the /config folder. If you still don't manage, please join our <0>Discord server and we'll be happy to help!", + "newInstall.finishTitle": "Finish 🎉", + "newInstall.fqdnAutoLetsEncryptInfo": "You seem to be using a domain name.
Let's Encrypt can automatically generate a certificate for you.", + "newInstall.hostnameInput.hostnameLabel": "Hostname (How would you like to access Cosmos?)", + "newInstall.hostnameInput.hostnamePlaceholder": "yourdomain.com, your ip, or localhost", + "newInstall.hostnamePointsToInfo": "This hostname is pointing to {{hostIp}}, check that it is your server IP!", + "newInstall.httpsText": "It is recommended to use Let's Encrypt to automatically provide HTTPS Certificates. This requires a valid domain name pointing to this server. If you don't have one, you can select \"Generate self-signed certificate\" in the dropdown. If you enable HTTPS, it will be effective after the next restart.", + "newInstall.httpsTitle": "HTTPS 🌐 (step 3/4)", + "newInstall.letsEncryptChoiceOnlyfqdnValidation": "Let\\'s Encrypt only accepts domain names", + "newInstall.linkToDocs": "Link to documentation", + "newInstall.loading": "Loading", + "newInstall.localAutoSelfSignedInfo": "You seem to be using an IP address or local domain.
You can use automatic Self-Signed certificates.", + "newInstall.previousButton": "Previous", + "newInstall.privCertInput.privCertLabel": "Private Certificate", + "newInstall.pubCertInput.pubCertLabel": "PublicCertificate", + "newInstall.setupUser.nicknameRootAdminNotAllowedValidation": "Benutzername darf nicht \\'admin\\' oder \\'root\\' sein", + "newInstall.setupUser.passwordMustMatchValidation": "Passwörter müssen übereinstimmen", + "newInstall.skipAction": "Skip", + "newInstall.sslEmailInput.sslEmailLabel": "Let's Encrypt Email", + "newInstall.usermgmt.disableButton": "Disable", + "newInstall.usermgmt.inviteUser.resendInviteButton": "Re-Send Invite ", + "newInstall.welcomeText": "First of all, thanks a lot for trying out Cosmos! And Welcome to the setup wizard. This wizard will guide you through the setup of Cosmos. It will take about 2-3 minutes and you will be ready to go.", + "newInstall.welcomeTitle": "Welcome! 💖", + "newInstall.whatIsCosmos": "Cosmos is using docker to run applications. It is optional, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker.", + "newInstall.wildcardLetsEncryptCheckbox.wildcardLetsEncryptLabel": "Use Wildcard Certificate for *.", + "newInstall.wildcardLetsEncryptError": "You have enabled wildcard certificates with Let's Encrypt. This only works if you use the DNS challenge! Please edit the DNS Provider text input." +} \ No newline at end of file diff --git a/client/src/utils/locales/i18n.jsx b/client/src/utils/locales/i18n.jsx new file mode 100644 index 00000000..3418d233 --- /dev/null +++ b/client/src/utils/locales/i18n.jsx @@ -0,0 +1,49 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import resourcesToBackend from 'i18next-resources-to-backend'; +import dayjs from 'dayjs'; + +i18n + // detect user language + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + //load resources as Backend (supports lazy loading) + .use(resourcesToBackend((language, namespace) => import(`./${language}/${namespace}.json`))) + // init i18next + .init({ + keySeparator: false, + debug: false, + fallbackLng: 'en', + supportedLngs: [ + 'en', + 'de', + 'de-CH' + ], + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + react: { + transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p', 'pre', 'b'] + } + }); + + export const getLanguage = () => { + return i18n.resolvedLanguage || i18n.language || + (typeof window !== 'undefined' && window.localStorage.i18nextLng) || + 'en'; + }; + + const locales = { + en: () => import('dayjs/locale/en'), + enGB: () => import('dayjs/locale/en-gb'), + de: () => import('dayjs/locale/de'), + deCH: () => import('dayjs/locale/de-ch'), + } + + export function dayjsLocale (language) { + locales[language.replace('-', '')]().then(() => dayjs.locale(language.toLowerCase())) + } + +export { i18n }; \ No newline at end of file diff --git a/client/src/utils/password-strength.jsx b/client/src/utils/password-strength.jsx index 320058f8..454cd779 100644 --- a/client/src/utils/password-strength.jsx +++ b/client/src/utils/password-strength.jsx @@ -8,13 +8,13 @@ const hasMixed = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A- const hasSpecial = (number) => new RegExp(/[~!@#$%\^&\*\(\)_\+=\-\{\[\}\]:;"'<,>\?\/]/).test(number); // set color based on password strength -export const strengthColor = (count) => { - if (count < 2) return { label: 'Poor', color: 'error.main' }; - if (count < 3) return { label: 'Weak', color: 'warning.main' }; - if (count < 4) return { label: 'Normal', color: 'warning.dark' }; - if (count < 5) return { label: 'Good', color: 'success.main' }; - if (count < 6) return { label: 'Strong', color: 'success.dark' }; - return { label: 'Poor', color: 'error.main' }; +export const strengthColor = (count, t) => { + if (count < 2) return { label: t('auth.genPwdStrength.poor'), color: 'error.main' }; + if (count < 3) return { label: t('auth.genPwdStrength.weak'), color: 'warning.main' }; + if (count < 4) return { label: t('auth.genPwdStrength.normal'), color: 'warning.dark' }; + if (count < 5) return { label: t('auth.genPwdStrength.good'), color: 'success.main' }; + if (count < 6) return { label: t('auth.genPwdStrength.strong'), color: 'success.dark' }; + return { label: t('auth.genPwdStrength.poor'), color: 'error.main' }; }; // password strength indicator diff --git a/client/src/utils/routes.jsx b/client/src/utils/routes.jsx index f4779e81..9bfdbc73 100644 --- a/client/src/utils/routes.jsx +++ b/client/src/utils/routes.jsx @@ -8,6 +8,7 @@ import { debounce, isDomain } from './indexs'; import * as API from '../api'; import { useEffect, useState } from 'react'; +import { Trans } from 'react-i18next'; export const sanitizeRoute = (_route) => { let route = { ..._route }; @@ -83,10 +84,10 @@ export const ValidateRouteSchema = Yup.object().shape({ Mode: Yup.string().required('Mode is required'), Target: Yup.string().required('Target is required').when('Mode', { is: 'SERVAPP', - then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'), + then: Yup.string().matches(/:[0-9]+$/, ), }).when('Mode', { is: 'PROXY', - then: Yup.string().matches(/^(https?:\/\/)/, 'Invalid Target, must start with http:// or https://'), + then: Yup.string().matches(/^(https?:\/\/)/, ), }), Host: Yup.string().when('UseHost', { @@ -165,7 +166,7 @@ export const HostnameChecker = ({hostname}) => { return <>{hostError && {hostError}} - {hostIp && This hostname is pointing to {hostIp}, make sure it is your server IP!} + {hostIp && } }; diff --git a/package.json b/package.json index e53e2546..f678dba3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "formik": "^2.2.9", "framer-motion": "^7.3.6", "history": "^5.3.0", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-resources-to-backend": "^1.2.1", "js-yaml": "^4.1.0", "lodash.map": "^4.6.0", "lodash.merge": "^4.6.2", @@ -52,6 +55,7 @@ "react-element-to-jsx-string": "^15.0.0", "react-intersection-observer": "^9.5.2", "react-lazyload": "^3.2.0", + "react-i18next": "^14.1.2", "react-material-ui-carousel": "^3.4.2", "react-number-format": "^4.9.4", "react-perfect-scrollbar": "^1.5.8", diff --git a/readme.md b/readme.md index 652fd145..0541754a 100644 --- a/readme.md +++ b/readme.md @@ -187,3 +187,5 @@ If you are having issues with the installation, please contact us on [Discord](h # Contribute [Contribute.md](./CONTRIBUTE.md) + +All of the Credits go to the original Project: [Cosmos Server](https://github.com/azukaar/Cosmos-Server) \ No newline at end of file diff --git a/src/docker/docker.go b/src/docker/docker.go index c252137c..ab6c064d 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -560,8 +560,9 @@ func CheckUpdatesAvailable() map[string]bool { utils.WriteNotification(utils.Notification{ Recipient: "admin", - Title: "Container Update", - Message: "Container " + container.Names[0][1:] + " updated to the latest version!", + Title: "header.notification.title.containerUpdate", + Message: "header.notification.message.containerUpdate", + Vars: container.Names[0][1:], Level: "info", Link: "/cosmos-ui/servapps/containers/" + container.Names[0][1:], }) diff --git a/src/httpServer.go b/src/httpServer.go index 6b8fa753..4e94bf1b 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -286,8 +286,9 @@ func InitServer() *mux.Router { utils.WriteNotification(utils.Notification{ Recipient: "admin", - Title: "Cosmos Certificate Renewed", - Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "), + Title: "header.notification.title.certificateRenewed", + Message: "header.notification.message.certificateRenewed", + Vars: strings.Join(domains, ", "), Level: "info", }) @@ -321,8 +322,9 @@ func InitServer() *mux.Router { utils.WriteNotification(utils.Notification{ Recipient: "admin", - Title: "Cosmos Certificate Renewed", - Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "), + Title: "header.notification.title.certificateRenewed", + Message: "header.notification.message.certificateRenewed", + Vars: strings.Join(domains, ", "), Level: "info", }) } diff --git a/src/metrics/alerts.go b/src/metrics/alerts.go index 2193383a..1559a9cb 100644 --- a/src/metrics/alerts.go +++ b/src/metrics/alerts.go @@ -182,8 +182,9 @@ more information.
`, alert.Severity, metric.Key)) } else if action.Type == "notification" { utils.WriteNotification(utils.Notification{ Recipient: "admin", - Title: "Alert triggered", - Message: "The alert \"" + alert.Name + "\" was triggered.", + Title: "header.notification.title.alertTriggered", + Message: "header.notification.message.alertTriggered", + Vars: alert.Name, Level: alert.Severity, Link: "/cosmos-ui/monitoring", }) diff --git a/src/utils/log.go b/src/utils/log.go index 62b55973..c0ad2c6c 100644 --- a/src/utils/log.go +++ b/src/utils/log.go @@ -69,8 +69,9 @@ func MajorError(message string, err error) { WriteNotification(Notification{ Recipient: "admin", - Title: "Server Error", + Title: "header.notification.title.serverError", Message: message + " : " + errStr, + Vars: "", Level: "error", }) } diff --git a/src/utils/notifications.go b/src/utils/notifications.go index ec2903c3..8eb27064 100644 --- a/src/utils/notifications.go +++ b/src/utils/notifications.go @@ -21,6 +21,7 @@ type Notification struct { ID primitive.ObjectID `bson:"_id,omitempty"` Title string Message string + Vars string Icon string Link string Date time.Time @@ -176,6 +177,7 @@ func WriteNotification(notification Notification) { BufferedDBWrite("notifications", map[string]interface{}{ "Title": notification.Title, "Message": notification.Message, + "Vars": notification.Vars, "Icon": notification.Icon, "Link": notification.Link, "Date": notification.Date, @@ -189,6 +191,7 @@ func WriteNotification(notification Notification) { BufferedDBWrite("notifications", map[string]interface{}{ "Title": notification.Title, "Message": notification.Message, + "Vars": notification.Vars, "Icon": notification.Icon, "Link": notification.Link, "Date": notification.Date,