mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 06:24:44 +02:00
Add front end translations
Co-authored-by: Miniontoby <tobias.gaarenstroom@gmail.com>
This commit is contained in:
parent
1bdef318f0
commit
8f2413dc7e
@ -4,6 +4,7 @@ import { Link, NavLink } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCogs, faLayerGroup, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
||||
import tw, { theme } from 'twin.macro';
|
||||
@ -33,6 +34,8 @@ const RightNavigation = styled.div`
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('strings');
|
||||
|
||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||
const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
@ -61,26 +64,26 @@ export default () => {
|
||||
</div>
|
||||
<RightNavigation className={'flex h-full items-center justify-center'}>
|
||||
<SearchContainer />
|
||||
<Tooltip placement={'bottom'} content={'Dashboard'}>
|
||||
<Tooltip placement={'bottom'} content={t<string>('dashboard')}>
|
||||
<NavLink to={'/'} exact>
|
||||
<FontAwesomeIcon icon={faLayerGroup} />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
{rootAdmin && (
|
||||
<Tooltip placement={'bottom'} content={'Admin'}>
|
||||
<Tooltip placement={'bottom'} content={t<string>('admin')}>
|
||||
<a href={'/admin'} rel={'noreferrer'}>
|
||||
<FontAwesomeIcon icon={faCogs} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip placement={'bottom'} content={'Account Settings'}>
|
||||
<Tooltip placement={'bottom'} content={t<string>('account_settings')}>
|
||||
<NavLink to={'/account'}>
|
||||
<span className={'flex items-center w-5 h-5'}>
|
||||
<Avatar.User />
|
||||
</span>
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
<Tooltip placement={'bottom'} content={'Sign Out'}>
|
||||
<Tooltip placement={'bottom'} content={t<string>('sign_out')}>
|
||||
<button onClick={onTriggerLogout}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
</button>
|
||||
|
@ -8,6 +8,7 @@ import { useStoreState } from 'easy-peasy';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Reaptcha from 'reaptcha';
|
||||
@ -18,6 +19,8 @@ interface Values {
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
@ -67,24 +70,24 @@ export default () => {
|
||||
initialValues={{ email: '' }}
|
||||
validationSchema={object().shape({
|
||||
email: string()
|
||||
.email('A valid email address must be provided to continue.')
|
||||
.required('A valid email address must be provided to continue.'),
|
||||
.email(t('forgot_password.required.email'))
|
||||
.required(t('forgot_password.required.email')),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||
<LoginFormContainer title={'Request Password Reset'} css={tw`w-full flex`}>
|
||||
<LoginFormContainer title={t('forgot_password.title')} css={tw`w-full flex`}>
|
||||
<Field
|
||||
light
|
||||
label={'Email'}
|
||||
description={
|
||||
'Enter your account email address to receive instructions on resetting your password.'
|
||||
t('forgot_password.label_help')
|
||||
}
|
||||
name={'email'}
|
||||
type={'email'}
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Send Email
|
||||
{t('forgot_password.button')}
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled && (
|
||||
@ -107,7 +110,7 @@ export default () => {
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
{t('return_to_login')}
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
|
@ -5,6 +5,7 @@ import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator } from 'easy-peasy';
|
||||
import { StaticContext } from 'react-router';
|
||||
import { useFormikContext, withFormik } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { FlashStore } from '@/state/flashes';
|
||||
import Field from '@/components/elements/Field';
|
||||
@ -23,20 +24,22 @@ type Props = OwnProps & {
|
||||
};
|
||||
|
||||
const LoginCheckpointContainer = () => {
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||
const [isMissingDevice, setIsMissingDevice] = useState(false);
|
||||
|
||||
return (
|
||||
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
||||
<LoginFormContainer title={t('checkpoint.title')} css={tw`w-full flex`}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light
|
||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||
title={isMissingDevice ? t('checkpoint.recovery_code') : t('checkpoint.authentication_code')}
|
||||
description={
|
||||
isMissingDevice
|
||||
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
|
||||
: 'Enter the two-factor token generated by your device.'
|
||||
? t('checkpoint.recovery_code_description')
|
||||
: t('checkpoint.authentication_code_description')
|
||||
}
|
||||
type={'text'}
|
||||
autoComplete={'one-time-code'}
|
||||
@ -45,7 +48,7 @@ const LoginCheckpointContainer = () => {
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button size={'xlarge'} type={'submit'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Continue
|
||||
{t('checkpoint.button')}
|
||||
</Button>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
@ -57,7 +60,7 @@ const LoginCheckpointContainer = () => {
|
||||
}}
|
||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
{!isMissingDevice ? "I've Lost My Device" : 'I Have My Device'}
|
||||
{!isMissingDevice ? t('checkpoint.lost_device') : t('checkpoint.have_device')}
|
||||
</span>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
@ -65,7 +68,7 @@ const LoginCheckpointContainer = () => {
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
{t('return_to_login')}
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
|
@ -5,6 +5,7 @@ import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
@ -17,6 +18,8 @@ interface Values {
|
||||
}
|
||||
|
||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
const { t } = useTranslation(['auth', 'strings']);
|
||||
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
@ -69,19 +72,19 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
onSubmit={onSubmit}
|
||||
initialValues={{ username: '', password: '' }}
|
||||
validationSchema={object().shape({
|
||||
username: string().required('A username or email must be provided.'),
|
||||
password: string().required('Please enter your account password.'),
|
||||
username: string().required(t('login.required.username_or_email')),
|
||||
password: string().required(t('login.required.password')),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
||||
<Field light type={'text'} label={'Username or Email'} name={'username'} disabled={isSubmitting} />
|
||||
<LoginFormContainer title={t('login.title')} css={tw`w-full flex`}>
|
||||
<Field light type={'text'} label={t('user_identifier', { ns: 'strings' })} name={'username'} disabled={isSubmitting} />
|
||||
<div css={tw`mt-6`}>
|
||||
<Field light type={'password'} label={'Password'} name={'password'} disabled={isSubmitting} />
|
||||
<Field light type={'password'} label={t('password', { ns: 'strings' })} name={'password'} disabled={isSubmitting} />
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
||||
Login
|
||||
{t('login.button')}
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled && (
|
||||
@ -104,7 +107,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
to={'/auth/password'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Forgot password?
|
||||
{t('forgot_password.label')}
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
|
@ -8,6 +8,7 @@ import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, ref, string } from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Field from '@/components/elements/Field';
|
||||
import Input from '@/components/elements/Input';
|
||||
import tw from 'twin.macro';
|
||||
@ -19,6 +20,8 @@ interface Values {
|
||||
}
|
||||
|
||||
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
||||
const { t } = useTranslation('auth');
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
@ -52,35 +55,35 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
||||
}}
|
||||
validationSchema={object().shape({
|
||||
password: string()
|
||||
.required('A new password is required.')
|
||||
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||
.required(t('reset_password.required.password'))
|
||||
.min(8, t('reset_password.validation.password')),
|
||||
passwordConfirmation: string()
|
||||
.required('Your new password does not match.')
|
||||
.required(t('reset_password.required.password_confirmation'))
|
||||
// @ts-expect-error this is valid
|
||||
.oneOf([ref('password'), null], 'Your new password does not match.'),
|
||||
.oneOf([ref('password'), null], t('reset_password.validation.password_confirmation')),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer title={'Reset Password'} css={tw`w-full flex`}>
|
||||
<LoginFormContainer title={t('reset_password.title')} css={tw`w-full flex`}>
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<label>{t('email')}</label>
|
||||
<Input value={email} isLight disabled />
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light
|
||||
label={'New Password'}
|
||||
label={t('reset_password.new_password')}
|
||||
name={'password'}
|
||||
type={'password'}
|
||||
description={'Passwords must be at least 8 characters in length.'}
|
||||
description={t('reset_password.requirement.password')}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field light label={'Confirm New Password'} name={'passwordConfirmation'} type={'password'} />
|
||||
<Field light label={t('reset_password.confirm_new_password')} name={'passwordConfirmation'} type={'password'} />
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button size={'xlarge'} type={'submit'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Reset Password
|
||||
{t('reset_password.button')}
|
||||
</Button>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
@ -88,7 +91,7 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||
>
|
||||
Return to Login
|
||||
{t('return_to_login')}
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
|
@ -10,12 +10,15 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { format } from 'date-fns';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import Code from '@/components/elements/Code';
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation(['dashboard/account', 'strings']);
|
||||
|
||||
const [deleteIdentifier, setDeleteIdentifier] = useState('');
|
||||
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -61,7 +64,7 @@ export default () => {
|
||||
</Dialog.Confirm>
|
||||
{keys.length === 0 ? (
|
||||
<p css={tw`text-center text-sm`}>
|
||||
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||
{loading ? t('loading', { ns: 'strings' }) : 'No API keys exist for this account.'}
|
||||
</p>
|
||||
) : (
|
||||
keys.map((key, index) => (
|
||||
@ -73,8 +76,8 @@ export default () => {
|
||||
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
||||
<p css={tw`text-sm break-words`}>{key.description}</p>
|
||||
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||
Last used:
|
||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||
{t('last_used', { ns: 'strings' })}:
|
||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : t('never', { ns: 'strings' })}
|
||||
</p>
|
||||
</div>
|
||||
<p css={tw`text-sm ml-4 hidden md:block`}>
|
||||
|
@ -9,6 +9,7 @@ import { breakpoint } from '@/theme';
|
||||
import styled from 'styled-components/macro';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Container = styled.div`
|
||||
${tw`flex flex-wrap`};
|
||||
@ -27,24 +28,25 @@ const Container = styled.div`
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('dashboard/account');
|
||||
const { state } = useLocation<undefined | { twoFactorRedirect?: boolean }>();
|
||||
|
||||
return (
|
||||
<PageContentBlock title={'Account Overview'}>
|
||||
<PageContentBlock title={t('title')}>
|
||||
{state?.twoFactorRedirect && (
|
||||
<MessageBox title={'2-Factor Required'} type={'error'}>
|
||||
Your account must have two-factor authentication enabled in order to continue.
|
||||
<MessageBox title={t('two_factor.required.title')} type={'error'}>
|
||||
{t('two_factor.required.description')}
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
<Container css={[tw`lg:grid lg:grid-cols-3 mb-10`, state?.twoFactorRedirect ? tw`mt-4` : tw`mt-10`]}>
|
||||
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
||||
<ContentBox title={t('password.title')} showFlashes={'account:password'}>
|
||||
<UpdatePasswordForm />
|
||||
</ContentBox>
|
||||
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title={'Update Email Address'} showFlashes={'account:email'}>
|
||||
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title={t('email.title')} showFlashes={'account:email'}>
|
||||
<UpdateEmailAddressForm />
|
||||
</ContentBox>
|
||||
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Two-Step Verification'}>
|
||||
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={t('two_factor.title')}>
|
||||
<ConfigureTwoFactorForm />
|
||||
</ContentBox>
|
||||
</Container>
|
||||
|
@ -13,8 +13,11 @@ import useSWR from 'swr';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
import Pagination from '@/components/elements/Pagination';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('dashboard/index');
|
||||
|
||||
const { search } = useLocation();
|
||||
const defaultPage = Number(new URLSearchParams(search).get('page') || '1');
|
||||
|
||||
@ -49,11 +52,11 @@ export default () => {
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<PageContentBlock title={'Dashboard'} showFlashKey={'dashboard'}>
|
||||
<PageContentBlock title={t('title')} showFlashKey={'dashboard'}>
|
||||
{rootAdmin && (
|
||||
<div css={tw`mb-2 flex justify-end items-center`}>
|
||||
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
||||
{showOnlyAdmin ? "Showing others' servers" : 'Showing your servers'}
|
||||
{showOnlyAdmin ? t('showing-others-servers') : t('showing-your-servers')}
|
||||
</p>
|
||||
<Switch
|
||||
name={'show_all_servers'}
|
||||
@ -74,8 +77,8 @@ export default () => {
|
||||
) : (
|
||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||
{showOnlyAdmin
|
||||
? 'There are no other servers to display.'
|
||||
: 'There are no servers associated with your account.'}
|
||||
? t('no-other-servers')
|
||||
: t('no-servers-associated')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import tw from 'twin.macro';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog';
|
||||
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
|
||||
@ -9,6 +10,8 @@ import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('dashboard/account');
|
||||
|
||||
const [tokens, setTokens] = useState<string[]>([]);
|
||||
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
|
||||
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
|
||||
@ -32,14 +35,14 @@ export default () => {
|
||||
<DisableTOTPDialog open={visible === 'disable'} onClose={() => setVisible(null)} />
|
||||
<p css={tw`text-sm`}>
|
||||
{isEnabled
|
||||
? 'Two-step verification is currently enabled on your account.'
|
||||
: 'You do not currently have two-step verification enabled on your account. Click the button below to begin configuring it.'}
|
||||
? t('two_factor.disable.help')
|
||||
: t('two_factor.enable.help')}
|
||||
</p>
|
||||
<div css={tw`mt-6`}>
|
||||
{isEnabled ? (
|
||||
<Button.Danger onClick={() => setVisible('disable')}>Disable Two-Step</Button.Danger>
|
||||
<Button.Danger onClick={() => setVisible('disable')}>{t('two_factor.disable.button')}</Button.Danger>
|
||||
) : (
|
||||
<Button onClick={() => setVisible('enable')}>Enable Two-Step</Button>
|
||||
<Button onClick={() => setVisible('enable')}>{t('two_factor.enable.button')}</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import asDialog from '@/hoc/asDialog';
|
||||
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
@ -10,6 +11,8 @@ import { useStoreActions } from '@/state/hooks';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
const DisableTOTPDialog = () => {
|
||||
const { t } = useTranslation(['dashboard/account', 'strings']);
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||
@ -41,7 +44,7 @@ const DisableTOTPDialog = () => {
|
||||
<form id={'disable-totp-form'} className={'mt-6'} onSubmit={submit}>
|
||||
<FlashMessageRender byKey={'account:two-step'} className={'-mt-2 mb-6'} />
|
||||
<label className={'block pb-1'} htmlFor={'totp-password'}>
|
||||
Password
|
||||
{t('password', { ns: 'strings' })}
|
||||
</label>
|
||||
<Input.Text
|
||||
id={'totp-password'}
|
||||
@ -51,14 +54,14 @@ const DisableTOTPDialog = () => {
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
<Button.Text onClick={close}>{t('cancel', { ns: 'strings' })}</Button.Text>
|
||||
<Tooltip
|
||||
delay={100}
|
||||
disabled={password.length > 0}
|
||||
content={'You must enter your account password to continue.'}
|
||||
content={t<string>('password.validation.account_password')}
|
||||
>
|
||||
<Button.Danger type={'submit'} form={'disable-totp-form'} disabled={submitting || !password.length}>
|
||||
Disable
|
||||
{t('disable', { ns: 'strings' })}
|
||||
</Button.Danger>
|
||||
</Tooltip>
|
||||
</Dialog.Footer>
|
||||
|
@ -3,6 +3,8 @@ import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
|
||||
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import tw from 'twin.macro';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
@ -20,6 +22,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
const { t } = useTranslation(['dashboard/account', 'strings']);
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@ -70,12 +74,11 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
</div>
|
||||
<CopyOnClick text={token?.secret}>
|
||||
<p className={'font-mono text-sm text-gray-100 text-center mt-2'}>
|
||||
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
|
||||
{token?.secret.match(/.{1,4}/g)!.join(' ') || t('loading', { ns: 'strings' })}
|
||||
</p>
|
||||
</CopyOnClick>
|
||||
<p id={'totp-code-description'} className={'mt-6'}>
|
||||
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
|
||||
code generated into the field below.
|
||||
{t('two_factor.setup.help')}
|
||||
</p>
|
||||
<Input.Text
|
||||
aria-labelledby={'totp-code-description'}
|
||||
@ -90,7 +93,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
pattern={'\\d{6}'}
|
||||
/>
|
||||
<label htmlFor={'totp-password'} className={'block mt-3'}>
|
||||
Account Password
|
||||
{t('account_password', { ns: 'strings' })}
|
||||
</label>
|
||||
<Input.Text
|
||||
variant={Input.Text.Variants.Loose}
|
||||
@ -100,7 +103,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
<Button.Text onClick={close}>{t('cancel', { ns: 'strings' })}</Button.Text>
|
||||
<Tooltip
|
||||
disabled={password.length > 0 && value.length === 6}
|
||||
content={
|
||||
@ -115,7 +118,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
type={'submit'}
|
||||
form={'enable-totp-form'}
|
||||
>
|
||||
Enable
|
||||
{t('enable', { ns: 'strings' })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Dialog.Footer>
|
||||
@ -124,7 +127,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
};
|
||||
|
||||
export default asDialog({
|
||||
title: 'Enable Two-Step Verification',
|
||||
title: i18n.t('dashboard/account:two_factor.setup.title') ?? 'Enable Two-Step Verification',
|
||||
description:
|
||||
"Help protect your account from unauthorized access. You'll be prompted for a verification code each time you sign in.",
|
||||
})(ConfigureTwoFactorForm);
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
@ -14,12 +15,14 @@ interface Values {
|
||||
password: string;
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
password: Yup.string().required('You must provide your current account password.'),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation(['dashboard/account', 'strings']);
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
password: Yup.string().required(t('password.validation.account_password')),
|
||||
});
|
||||
|
||||
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
|
||||
const updateEmail = useStoreActions((state: Actions<ApplicationStore>) => state.user.updateUserEmail);
|
||||
|
||||
@ -33,14 +36,14 @@ export default () => {
|
||||
addFlash({
|
||||
type: 'success',
|
||||
key: 'account:email',
|
||||
message: 'Your primary email has been updated.',
|
||||
message: t('email.updated'),
|
||||
})
|
||||
)
|
||||
.catch((error) =>
|
||||
addFlash({
|
||||
type: 'error',
|
||||
key: 'account:email',
|
||||
title: 'Error',
|
||||
title: t('error', { ns: 'strings' }),
|
||||
message: httpErrorToHuman(error),
|
||||
})
|
||||
)
|
||||
@ -56,17 +59,17 @@ export default () => {
|
||||
<React.Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||
<Form css={tw`m-0`}>
|
||||
<Field id={'current_email'} type={'email'} name={'email'} label={'Email'} />
|
||||
<Field id={'current_email'} type={'email'} name={'email'} label={t('email', { ns: 'strings' })} />
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
label={'Confirm Password'}
|
||||
label={t('confirm_password', { ns: 'strings' })}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
|
||||
<Button disabled={isSubmitting || !isValid}>{t('email.button')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
|
@ -3,6 +3,7 @@ import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import updateAccountPassword from '@/api/account/updateAccountPassword';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
@ -16,19 +17,21 @@ interface Values {
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
current: Yup.string().min(1).required('You must provide your current password.'),
|
||||
password: Yup.string().min(8).required(),
|
||||
confirmPassword: Yup.string().test(
|
||||
'password',
|
||||
'Password confirmation does not match the password you entered.',
|
||||
function (value) {
|
||||
return value === this.parent.password;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation(['dashboard/account', 'strings']);
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
current: Yup.string().min(1).required('You must provide your current password.'),
|
||||
password: Yup.string().min(8).required(),
|
||||
confirmPassword: Yup.string().test(
|
||||
'password',
|
||||
'Password confirmation does not match the password you entered.',
|
||||
function (value) {
|
||||
return value === this.parent.password;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
@ -47,7 +50,7 @@ export default () => {
|
||||
addFlash({
|
||||
key: 'account:password',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
title: t('error', { ns: 'strings' }),
|
||||
message: httpErrorToHuman(error),
|
||||
})
|
||||
)
|
||||
@ -69,16 +72,16 @@ export default () => {
|
||||
id={'current_password'}
|
||||
type={'password'}
|
||||
name={'current'}
|
||||
label={'Current Password'}
|
||||
label={t('current_password', { ns: 'strings' })}
|
||||
/>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
id={'new_password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
label={'New Password'}
|
||||
label={t('new_password', { ns: 'strings' })}
|
||||
description={
|
||||
'Your new password should be at least 8 characters in length and unique to this website.'
|
||||
t('password.requirements')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -87,11 +90,11 @@ export default () => {
|
||||
id={'confirm_new_password'}
|
||||
type={'password'}
|
||||
name={'confirmPassword'}
|
||||
label={'Confirm New Password'}
|
||||
label={t('confirm_password', { ns: 'strings' })}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button disabled={isSubmitting || !isValid}>Update Password</Button>
|
||||
<Button disabled={isSubmitting || !isValid}>{t('password.button')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
|
@ -4,8 +4,11 @@ import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import SearchModal from '@/components/dashboard/search/SearchModal';
|
||||
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('strings');
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
@ -19,7 +22,7 @@ export default () => {
|
||||
return (
|
||||
<>
|
||||
{visible && <SearchModal appear visible={visible} onDismissed={() => setVisible(false)} />}
|
||||
<Tooltip placement={'bottom'} content={'Search'}>
|
||||
<Tooltip placement={'bottom'} content={t<string>('search')}>
|
||||
<div className={'navigation-link'} onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { object, string } from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import debounce from 'debounce';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
@ -46,6 +47,8 @@ const SearchWatcher = () => {
|
||||
};
|
||||
|
||||
export default ({ ...props }: Props) => {
|
||||
const { t } = useTranslation('search');
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isAdmin = useStoreState((state) => state.user.data!.rootAdmin);
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
@ -80,7 +83,7 @@ export default ({ ...props }: Props) => {
|
||||
<Formik
|
||||
onSubmit={search}
|
||||
validationSchema={object().shape({
|
||||
term: string().min(3, 'Please enter at least three characters to begin searching.'),
|
||||
term: string().min(3, t('validation')),
|
||||
})}
|
||||
initialValues={{ term: '' } as Values}
|
||||
>
|
||||
@ -89,8 +92,8 @@ export default ({ ...props }: Props) => {
|
||||
<Form>
|
||||
<FormikFieldWrapper
|
||||
name={'term'}
|
||||
label={'Search term'}
|
||||
description={'Enter a server name, uuid, or allocation to begin searching.'}
|
||||
label={t('term.label')}
|
||||
description={t('term.description')}
|
||||
>
|
||||
<SearchWatcher />
|
||||
<InputSpinner visible={isSubmitting}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@/components/elements/Button';
|
||||
import asModal from '@/hoc/asModal';
|
||||
import ModalContext from '@/context/ModalContext';
|
||||
@ -12,6 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onConfirmed }) => {
|
||||
const { t } = useTranslation('strings');
|
||||
const { dismiss } = useContext(ModalContext);
|
||||
|
||||
return (
|
||||
@ -20,7 +22,7 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
|
||||
<div css={tw`text-neutral-300`}>{children}</div>
|
||||
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
||||
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button color={'red'} css={tw`w-full sm:w-auto mt-4 sm:mt-0 sm:ml-4`} onClick={() => onConfirmed()}>
|
||||
{buttonText}
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import Icon from '@/components/elements/Icon';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
@ -27,7 +28,7 @@ class ErrorBoundary extends React.Component<{}, State> {
|
||||
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
|
||||
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`} />
|
||||
<p css={tw`text-sm text-neutral-100`}>
|
||||
An error was encountered by the application while rendering this view. Try refreshing the page.
|
||||
<Trans i18nKey="error_rendering_view" defaults="An error was encountered by the application while rendering this view. Try refreshing the page." />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
|
||||
@ -7,11 +8,13 @@ export interface RequireServerPermissionProps {
|
||||
}
|
||||
|
||||
const RequireServerPermission: React.FC<RequireServerPermissionProps> = ({ children, permissions }) => {
|
||||
const { t } = useTranslation('strings');
|
||||
|
||||
return (
|
||||
<Can
|
||||
action={permissions}
|
||||
renderOnError={
|
||||
<ServerError title={'Access Denied'} message={'You do not have permission to access this page.'} />
|
||||
<ServerError title={t('access_denied.title')} message={t('access_denied.message')} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
|
Loading…
x
Reference in New Issue
Block a user