Add front end translations

Co-authored-by: Miniontoby <tobias.gaarenstroom@gmail.com>
This commit is contained in:
Lance Pioch 2024-04-25 23:33:08 -04:00
parent 1bdef318f0
commit 8f2413dc7e
18 changed files with 148 additions and 98 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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:&nbsp;
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
{t('last_used', { ns: 'strings' })}:&nbsp;
{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`}>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}>

View File

@ -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}

View File

@ -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>

View File

@ -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}