diff --git a/resources/scripts/components/NavigationBar.tsx b/resources/scripts/components/NavigationBar.tsx index 89180b14b..f784ddfff 100644 --- a/resources/scripts/components/NavigationBar.tsx +++ b/resources/scripts/components/NavigationBar.tsx @@ -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 () => { - + ('dashboard')}> {rootAdmin && ( - + ('admin')}> )} - + ('account_settings')}> - + ('sign_out')}> diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index 76ddf1992..73aff8100 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -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(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 }) => ( - +
{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')}
diff --git a/resources/scripts/components/auth/LoginCheckpointContainer.tsx b/resources/scripts/components/auth/LoginCheckpointContainer.tsx index 66a6fb8f4..5de3a3b4b 100644 --- a/resources/scripts/components/auth/LoginCheckpointContainer.tsx +++ b/resources/scripts/components/auth/LoginCheckpointContainer.tsx @@ -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(); const [isMissingDevice, setIsMissingDevice] = useState(false); return ( - +
{
@@ -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')}
@@ -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')}
diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx index 453a27ecc..618bb8183 100644 --- a/resources/scripts/components/auth/LoginContainer.tsx +++ b/resources/scripts/components/auth/LoginContainer.tsx @@ -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(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 }) => ( - - + +
- +
{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')}
diff --git a/resources/scripts/components/auth/ResetPasswordContainer.tsx b/resources/scripts/components/auth/ResetPasswordContainer.tsx index 86f95abf1..5940118b5 100644 --- a/resources/scripts/components/auth/ResetPasswordContainer.tsx +++ b/resources/scripts/components/auth/ResetPasswordContainer.tsx @@ -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) => 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 }) => ( - +
- +
- +
@@ -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')}
diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index 282f19092..6a3ce0d64 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -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([]); const [loading, setLoading] = useState(true); @@ -61,7 +64,7 @@ export default () => { {keys.length === 0 ? (

- {loading ? 'Loading...' : 'No API keys exist for this account.'} + {loading ? t('loading', { ns: 'strings' }) : 'No API keys exist for this account.'}

) : ( keys.map((key, index) => ( @@ -73,8 +76,8 @@ export default () => {

{key.description}

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

- {showOnlyAdmin ? "Showing others' servers" : 'Showing your servers'} + {showOnlyAdmin ? t('showing-others-servers') : t('showing-your-servers')}

{ ) : (

{showOnlyAdmin - ? 'There are no other servers to display.' - : 'There are no servers associated with your account.'} + ? t('no-other-servers') + : t('no-servers-associated')}

) } diff --git a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx index a59977f5f..0f6e846e5 100644 --- a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx +++ b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx @@ -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([]); const [visible, setVisible] = useState<'enable' | 'disable' | null>(null); const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp); @@ -32,14 +35,14 @@ export default () => { setVisible(null)} />

{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')}

{isEnabled ? ( - setVisible('disable')}>Disable Two-Step + setVisible('disable')}>{t('two_factor.disable.button')} ) : ( - + )}
diff --git a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx index 125376eb5..547bab4ff 100644 --- a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx @@ -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 = () => {
{ onChange={(e) => setPassword(e.currentTarget.value)} /> - Cancel + {t('cancel', { ns: 'strings' })} 0} - content={'You must enter your account password to continue.'} + content={t('password.validation.account_password')} > - Disable + {t('disable', { ns: 'strings' })} diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index 836b2ef87..b1635a32d 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx @@ -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) => {

- {token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'} + {token?.secret.match(/.{1,4}/g)!.join(' ') || t('loading', { ns: 'strings' })}

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

{ pattern={'\\d{6}'} /> { onChange={(e) => setPassword(e.currentTarget.value)} /> - Cancel + {t('cancel', { ns: 'strings' })} 0 && value.length === 6} content={ @@ -115,7 +118,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => { type={'submit'} form={'enable-totp-form'} > - Enable + {t('enable', { ns: 'strings' })} @@ -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); diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx index 462a37f1a..66b57983b 100644 --- a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx @@ -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) => state.user.data); const updateEmail = useStoreActions((state: Actions) => 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 () => { - +
- +
diff --git a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx index 707ee72d7..2723b9da5 100644 --- a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx @@ -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) => state.user.data); const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => 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' })} />
@@ -87,11 +90,11 @@ export default () => { id={'confirm_new_password'} type={'password'} name={'confirmPassword'} - label={'Confirm New Password'} + label={t('confirm_password', { ns: 'strings' })} />
- +
diff --git a/resources/scripts/components/dashboard/search/SearchContainer.tsx b/resources/scripts/components/dashboard/search/SearchContainer.tsx index 4da0f3a3f..1ec262128 100644 --- a/resources/scripts/components/dashboard/search/SearchContainer.tsx +++ b/resources/scripts/components/dashboard/search/SearchContainer.tsx @@ -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 && setVisible(false)} />} - + ('search')}>
setVisible(true)}>
diff --git a/resources/scripts/components/dashboard/search/SearchModal.tsx b/resources/scripts/components/dashboard/search/SearchModal.tsx index 1335cff43..c6f796a80 100644 --- a/resources/scripts/components/dashboard/search/SearchModal.tsx +++ b/resources/scripts/components/dashboard/search/SearchModal.tsx @@ -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(null); const isAdmin = useStoreState((state) => state.user.data!.rootAdmin); const [servers, setServers] = useState([]); @@ -80,7 +83,7 @@ export default ({ ...props }: Props) => { @@ -89,8 +92,8 @@ export default ({ ...props }: Props) => {
diff --git a/resources/scripts/components/elements/ConfirmationModal.tsx b/resources/scripts/components/elements/ConfirmationModal.tsx index 52f9f9e3c..039b97078 100644 --- a/resources/scripts/components/elements/ConfirmationModal.tsx +++ b/resources/scripts/components/elements/ConfirmationModal.tsx @@ -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 = ({ title, children, buttonText, onConfirmed }) => { + const { t } = useTranslation('strings'); const { dismiss } = useContext(ModalContext); return ( @@ -20,7 +22,7 @@ const ConfirmationModal: React.FC = ({ title, children, buttonText, onCon
{children}
diff --git a/resources/scripts/hoc/RequireServerPermission.tsx b/resources/scripts/hoc/RequireServerPermission.tsx index cb7da2b31..5fadf098d 100644 --- a/resources/scripts/hoc/RequireServerPermission.tsx +++ b/resources/scripts/hoc/RequireServerPermission.tsx @@ -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 = ({ children, permissions }) => { + const { t } = useTranslation('strings'); + return ( + } > {children}