Webatrice: i18n login screen (#4584)

* i18n: login container and form

* i18n: activate, host, and register forms

* i18n: reset password forms

* i18n: login dialogs, ICU formatting

* i18n: login containers and components

Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
Jeremy Letto 2022-03-02 22:34:57 -06:00 committed by GitHub
parent baaf261116
commit f5b973e15c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 424 additions and 99 deletions

View file

@ -1248,6 +1248,95 @@
} }
} }
}, },
"@formatjs/ecma402-abstract": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.3.tgz",
"integrity": "sha512-kP/Buv5vVFMAYLHNvvUzr0lwRTU0u2WTy44Tqwku1X3C3lJ5dKqDCYVqA8wL+Y19Bq+MwHgxqd5FZJRCIsLRyQ==",
"dev": true,
"requires": {
"@formatjs/intl-localematcher": "0.2.24",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@formatjs/fast-memoize": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
"integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@formatjs/icu-messageformat-parser": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.18.tgz",
"integrity": "sha512-vquIzsAJJmZ5jWVH8dEgUKcbG4yu3KqtyPet+q35SW5reLOvblkfeCXTRW2TpIwNXzdVqsJBwjbTiRiSU9JxwQ==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.11.3",
"@formatjs/icu-skeleton-parser": "1.3.5",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@formatjs/icu-skeleton-parser": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.5.tgz",
"integrity": "sha512-Nhyo2/6kG7ZfgeEfo02sxviOuBcvtzH6SYUharj3DLCDJH3A/4OxkKcmx/2PWGX4bc6iSieh+FA94CsKDxnZBQ==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.11.3",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@formatjs/intl-localematcher": {
"version": "0.2.24",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.24.tgz",
"integrity": "sha512-K/HRGo6EMnCbhpth/y3u4rW4aXkmQNqRe1L2G+Y5jNr3v0gYhvaucV8WixNju/INAMbPBlbsRBRo/nfjnoOnxQ==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"@gar/promisify": { "@gar/promisify": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@ -8217,6 +8306,12 @@
"@babel/runtime": "^7.14.6" "@babel/runtime": "^7.14.6"
} }
}, },
"i18next-icu": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/i18next-icu/-/i18next-icu-2.0.3.tgz",
"integrity": "sha512-sZ0VCWDnHysUYQL8j/0rVOxv6rLR+SBoaqQQ2UVNfLyJCuf/bAjYPkoUQgyuDkWFo1xZjeCf4G6GBNr7gD61bQ==",
"dev": true
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -8370,6 +8465,26 @@
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
} }
}, },
"intl-messageformat": {
"version": "9.11.4",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.11.4.tgz",
"integrity": "sha512-77TSkNubIy/hsapz6LQpyR6OADcxhWdhSaboPb5flMaALCVkPvAIxr48AlPqaMl4r1anNcvR9rpLWVdwUY1IKg==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.11.3",
"@formatjs/fast-memoize": "1.2.1",
"@formatjs/icu-messageformat-parser": "2.0.18",
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",

View file

@ -86,6 +86,8 @@
"@typescript-eslint/parser": "^5.3.1", "@typescript-eslint/parser": "^5.3.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"identity-obj-proxy": "^3.0.0" "i18next-icu": "^2.0.3",
"identity-obj-proxy": "^3.0.0",
"intl-messageformat": "^9.11.4"
} }
} }

View file

@ -1,6 +1,27 @@
{ {
"Common": { "Common": {
"language": "Translate into English.", "language": "Translate into English.",
"disconnect": "Disconnect" "disconnect": "Disconnect",
"label": {
"confirmPassword": "Confirm Password",
"confirmSure": "Are you sure?",
"country": "Country",
"delete": "Delete",
"email": "Email",
"hostName": "Host Name",
"hostAddress": "Host Address",
"password": "Password",
"passwordAgain": "Password Again",
"port": "Port",
"realName": "Real Name",
"saveChanges": "Save Changes",
"token": "Token",
"username": "Username"
},
"validation": {
"minChars": "Minimum of {count} {count, plural, one {character} other {characters}} required",
"passwordsMustMatch": "Passwords don't match",
"required": "Required"
}
} }
} }

View file

@ -0,0 +1,7 @@
{
"KnownHosts": {
"label": "Host",
"add": "Add new host",
"toast": "Host successfully {mode, select, created {created} deleted {deleted} other {edited}}."
}
}

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Select, MenuItem } from '@material-ui/core'; import { Select, MenuItem } from '@material-ui/core';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import FormControl from '@material-ui/core/FormControl'; import FormControl from '@material-ui/core/FormControl';
@ -33,6 +34,7 @@ const KnownHosts = (props) => {
const { input: { onChange }, meta, disabled } = props; const { input: { onChange }, meta, disabled } = props;
const { touched, error, warning } = meta; const { touched, error, warning } = meta;
const classes = useStyles(); const classes = useStyles();
const { t } = useTranslation();
const [hostsState, setHostsState] = useState({ const [hostsState, setHostsState] = useState({
hosts: [], hosts: [],
@ -168,7 +170,7 @@ const KnownHosts = (props) => {
</div> </div>
) } ) }
<InputLabel id='KnownHosts-select'>Host</InputLabel> <InputLabel id='KnownHosts-select'>{ t('KnownHosts.label') }</InputLabel>
<Select <Select
id='KnownHosts-select' id='KnownHosts-select'
labelId='KnownHosts-label' labelId='KnownHosts-label'
@ -181,7 +183,7 @@ const KnownHosts = (props) => {
disabled={disabled} disabled={disabled}
> >
<Button value={hostsState.selectedHost} onClick={openAddKnownHostDialog}> <Button value={hostsState.selectedHost} onClick={openAddKnownHostDialog}>
<span>Add new host</span> <span>{ t('KnownHosts.add') }</span>
<AddIcon fontSize='small' color='primary' /> <AddIcon fontSize='small' color='primary' />
</Button> </Button>
@ -213,9 +215,9 @@ const KnownHosts = (props) => {
onSubmit={handleDialogSubmit} onSubmit={handleDialogSubmit}
handleClose={closeKnownHostDialog} handleClose={closeKnownHostDialog}
/> />
<Toast open={showCreateToast} onClose={() => setShowCreateToast(false)}>Host successfully created.</Toast> <Toast open={showCreateToast} onClose={() => setShowCreateToast(false)}>{ t('KnownHosts.toast', { mode: 'created' }) }</Toast>
<Toast open={showDeleteToast} onClose={() => setShowDeleteToast(false)}>Host successfully deleted.</Toast> <Toast open={showDeleteToast} onClose={() => setShowDeleteToast(false)}>{ t('KnownHosts.toast', { mode: 'deleted' }) }</Toast>
<Toast open={showEditToast} onClose={() => setShowEditToast(false)}>Host successfully edited.</Toast> <Toast open={showEditToast} onClose={() => setShowEditToast(false)}>{ t('KnownHosts.toast', { mode: 'edited' }) }</Toast>
</div> </div>
) )
}; };

View file

@ -0,0 +1,6 @@
{
"InitializeContainer": {
"title": "DID YOU KNOW",
"subtitle": "<1>Cockatrice is run by volunteers</1><1>that love card games!</1>"
}
}

View file

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom'; import { Redirect, withRouter } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
@ -24,6 +25,7 @@ const useStyles = makeStyles(theme => ({
const Initialize = ({ initialized }: InitializeProps) => { const Initialize = ({ initialized }: InitializeProps) => {
const classes = useStyles(); const classes = useStyles();
const { t } = useTranslation();
return initialized return initialized
? <Redirect from="*" to={RouteEnum.LOGIN} /> ? <Redirect from="*" to={RouteEnum.LOGIN} />
@ -31,9 +33,11 @@ const Initialize = ({ initialized }: InitializeProps) => {
<div className={'Initialize ' + classes.root}> <div className={'Initialize ' + classes.root}>
<div className='Initialize-content'> <div className='Initialize-content'>
<img src={Images.Logo} alt="logo" /> <img src={Images.Logo} alt="logo" />
<Typography variant="subtitle1" className='subtitle'>DID YOU KNOW</Typography> <Typography variant="subtitle1" className='subtitle'>{ t('InitializeContainer.title') }</Typography>
<Typography variant="subtitle2">Cockatrice is run by volunteers</Typography> <Trans i18nKey="InitializeContainer.subtitle">
<Typography variant="subtitle2">that love card games!</Typography> <Typography variant="subtitle2"></Typography>
<Typography variant="subtitle2"></Typography>
</Trans>
</div> </div>
<div className="Initialize-graphics"> <div className="Initialize-graphics">

View file

@ -1,6 +1,22 @@
{ {
"LoginContainer": { "LoginContainer": {
"title": "Login", "header": {
"subtitle": "A cross-platform virtual tabletop for multiplayer card games." "title": "Login",
"subtitle": "A cross-platform virtual tabletop for multiplayer card games."
},
"footer": {
"registerPrompt": "Not registered yet?",
"registerAction": "Create an account",
"credit": "Cockatrice is an open source project",
"version": "Version"
},
"content": {
"subtitle1": "Play multiplayer card games online.",
"subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free."
},
"toasts": {
"passwordResetSuccessToast": "Password Reset Successfully",
"accountActivationSuccess": "Account Activated Successfully"
}
} }
} }

View file

@ -72,8 +72,11 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
}); });
const [userToResetPassword, setUserToResetPassword] = useState(null); const [userToResetPassword, setUserToResetPassword] = useState(null);
const passwordResetToast = useToast({ key: 'password-reset-success', children: 'Password Reset Successfully' }) const passwordResetToast = useToast({ key: 'password-reset-success', children: t('LoginContainer.toasts.passwordResetSuccess') });
const accountActivatedToast = useToast({ key: 'account-activation-success', children: 'Account Activated Successfully' }) const accountActivatedToast = useToast({
key: 'account-activation-success',
children: t('LoginContainer.toasts.accountActivationSuccess')
});
useReduxEffect(() => { useReduxEffect(() => {
closeRequestPasswordResetDialog(); closeRequestPasswordResetDialog();
@ -227,8 +230,8 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
<img src={Images.Logo} alt="logo" /> <img src={Images.Logo} alt="logo" />
<span>COCKATRICE</span> <span>COCKATRICE</span>
</div> </div>
<Typography variant="h1">{ t('LoginContainer.title') }</Typography> <Typography variant="h1">{ t('LoginContainer.header.title') }</Typography>
<Typography variant="subtitle1">{ t('LoginContainer.subtitle') }</Typography> <Typography variant="subtitle1">{ t('LoginContainer.header.subtitle') }</Typography>
<div className="login-form"> <div className="login-form">
<LoginForm <LoginForm
onSubmit={handleLogin} onSubmit={handleLogin}
@ -247,17 +250,17 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
<div className="login-footer"> <div className="login-footer">
<div className="login-footer__register"> <div className="login-footer__register">
<span>Not registered yet?</span> <span>{ t('LoginContainer.footer.registerPrompt') }</span>
<Button color="primary" onClick={openRegistrationDialog}>Create an account</Button> <Button color="primary" onClick={openRegistrationDialog}>{ t('LoginContainer.footer.registerAction') }</Button>
</div> </div>
<Typography variant="subtitle2"> <Typography variant="subtitle2">
Cockatrice is an open source project. { new Date().getUTCFullYear() } { t('LoginContainer.footer.credit') } - { new Date().getUTCFullYear() }
</Typography> </Typography>
{ {
serverProps.REACT_APP_VERSION && ( serverProps.REACT_APP_VERSION && (
<Typography variant="subtitle2"> <Typography variant="subtitle2">
Version: { serverProps.REACT_APP_VERSION } { t('LoginContainer.footer.version') }: { serverProps.REACT_APP_VERSION }
</Typography> </Typography>
) )
} }
@ -298,10 +301,8 @@ const Login = ({ state, description, connectOptions }: LoginProps) => {
</div> </div>
</div> </div>
{ /*<img src={loginGraphic} className="login-content__description-image"/>*/} { /*<img src={loginGraphic} className="login-content__description-image"/>*/}
<p className="login-content__description-subtitle1">Play multiplayer card games online.</p> <p className="login-content__description-subtitle1">{ t('LoginContainer.content.subtitle1') }</p>
<p className="login-content__description-subtitle2"> <p className="login-content__description-subtitle2">{ t('LoginContainer.content.subtitle2') }</p>
Cross-platform virtual tabletop for multiplayer card games. Forever free.
</p>
</div> </div>
</div> </div>
</Paper> </Paper>

View file

@ -0,0 +1,7 @@
{
"UnsupportedContainer": {
"title": "Unsupported Browser",
"subtitle1": "Please update your browser and/or check your permissions.",
"subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features."
}
}

View file

@ -1,4 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
@ -6,15 +7,17 @@ import Typography from '@material-ui/core/Typography';
import './Unsupported.css'; import './Unsupported.css';
const Unsupported = () => { const Unsupported = () => {
const { t } = useTranslation();
return ( return (
<div className='Unsupported'> <div className='Unsupported'>
<Paper className='Unsupported-paper'> <Paper className='Unsupported-paper'>
<div className='Unsupported-paper__header'> <div className='Unsupported-paper__header'>
<Typography variant="h1">Unsupported Browser</Typography> <Typography variant="h1">{ t('UnsupportedContainer.title') }</Typography>
<Typography variant="subtitle1">Please update your browser and/or check your permissions.</Typography> <Typography variant="subtitle1">{ t('UnsupportedContainer.subtitle1') }</Typography>
</div> </div>
<Typography variant="subtitle2">Note: Private browsing causes some browsers to disable certain permissions or features.</Typography> <Typography variant="subtitle2">{ t('UnsupportedContainer.subtitle2') }</Typography>
</Paper> </Paper>
</div> </div>
); );

View file

@ -0,0 +1,7 @@
{
"AccountActivationDialog": {
"title": "Account Activation",
"subtitle1": "Your account has not been activated yet.",
"subtitle2": "You need to provide the activation token received in the activation email."
}
}

View file

@ -5,12 +5,15 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useTranslation } from 'react-i18next';
import { AccountActivationForm } from 'forms'; import { AccountActivationForm } from 'forms';
import './AccountActivationDialog.css'; import './AccountActivationDialog.css';
const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
const { t } = useTranslation();
const handleOnClose = () => { const handleOnClose = () => {
handleClose(); handleClose();
} }
@ -18,7 +21,7 @@ const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any
return ( return (
<Dialog onClose={handleOnClose} open={isOpen}> <Dialog onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className="dialog-title"> <DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Account Activation</Typography> <Typography variant="h6">{ t('AccountActivationDialog.title') }</Typography>
{handleOnClose ? ( {handleOnClose ? (
<IconButton onClick={handleOnClose}> <IconButton onClick={handleOnClose}>
@ -28,8 +31,8 @@ const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<div className="content"> <div className="content">
<Typography variant='subtitle1'>Your account has not been activated yet.</Typography> <Typography variant='subtitle1'>{ t('AccountActivationDialog.subtitle1') }</Typography>
<Typography variant='subtitle1'>You need to provide the activation token received in the activation email.</Typography> <Typography variant='subtitle1'>{ t('AccountActivationDialog.subtitle2') }</Typography>
</div> </div>
<AccountActivationForm onSubmit={onSubmit}></AccountActivationForm> <AccountActivationForm onSubmit={onSubmit}></AccountActivationForm>

View file

@ -0,0 +1,6 @@
{
"KnownHostDialog": {
"title": "{mode, select, edit {Edit} other {Add}} Known Host",
"subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list."
}
}

View file

@ -7,6 +7,7 @@ import { makeStyles } from '@material-ui/core/styles';
import AddIcon from '@material-ui/icons/Add'; import AddIcon from '@material-ui/icons/Add';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useTranslation } from 'react-i18next';
import { KnownHostForm } from 'forms'; import { KnownHostForm } from 'forms';
@ -22,6 +23,9 @@ const useStyles = makeStyles(theme => ({
const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any) => { const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any) => {
const classes = useStyles(); const classes = useStyles();
const { t } = useTranslation();
const mode = host ? 'edit' : 'add';
const handleOnClose = () => { const handleOnClose = () => {
if (handleClose) { if (handleClose) {
@ -33,7 +37,7 @@ const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any)
<Dialog className={'KnownHostDialog ' + classes.root} onClose={handleOnClose} open={isOpen}> <Dialog className={'KnownHostDialog ' + classes.root} onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className='dialog-title'> <DialogTitle disableTypography className='dialog-title'>
<div className='dialog-title__wrapper'> <div className='dialog-title__wrapper'>
<Typography variant='h2'>{ host ? 'Edit' : 'Add' } Known Host</Typography> <Typography variant='h2'>{ t('KnownHostDialog.title', { mode }) }</Typography>
{handleClose ? ( {handleClose ? (
<IconButton onClick={handleClose}> <IconButton onClick={handleClose}>
@ -44,7 +48,7 @@ const KnownHostDialog = ({ handleClose, onRemove, onSubmit, isOpen, host }: any)
</DialogTitle> </DialogTitle>
<DialogContent className='dialog-content'> <DialogContent className='dialog-content'>
<Typography className='dialog-content__subtitle' variant='subtitle1'> <Typography className='dialog-content__subtitle' variant='subtitle1'>
Adding a new host allows you to connect to different servers. Enter the details below to your host list. { t('KnownHostDialog.subtitle') }
</Typography> </Typography>
<KnownHostForm onRemove={onRemove} onSubmit={onSubmit} host={host}></KnownHostForm> <KnownHostForm onRemove={onRemove} onSubmit={onSubmit} host={host}></KnownHostForm>
</DialogContent> </DialogContent>

View file

@ -0,0 +1,5 @@
{
"RegistrationDialog": {
"title": "Create New Account"
}
}

View file

@ -5,12 +5,15 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useTranslation } from 'react-i18next';
import { RegisterForm } from 'forms'; import { RegisterForm } from 'forms';
import './RegistrationDialog.css'; import './RegistrationDialog.css';
const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
const { t } = useTranslation();
const handleOnClose = () => { const handleOnClose = () => {
handleClose(); handleClose();
} }
@ -18,7 +21,7 @@ const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) =>
return ( return (
<Dialog className="RegistrationDialog" onClose={handleOnClose} open={isOpen} maxWidth='xl'> <Dialog className="RegistrationDialog" onClose={handleOnClose} open={isOpen} maxWidth='xl'>
<DialogTitle disableTypography className="dialog-title"> <DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Create New Account</Typography> <Typography variant="h6">{ t('RegistrationDialog.title') }</Typography>
{handleOnClose ? ( {handleOnClose ? (
<IconButton onClick={handleOnClose}> <IconButton onClick={handleOnClose}>

View file

@ -0,0 +1,5 @@
{
"RequestPasswordResetDialog": {
"title": "Request Password Reset"
}
}

View file

@ -5,12 +5,15 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useTranslation } from 'react-i18next';
import { RequestPasswordResetForm } from 'forms'; import { RequestPasswordResetForm } from 'forms';
import './RequestPasswordResetDialog.css'; import './RequestPasswordResetDialog.css';
const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, skipTokenRequest }: any) => { const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, skipTokenRequest }: any) => {
const { t } = useTranslation();
const handleOnClose = () => { const handleOnClose = () => {
handleClose(); handleClose();
} }
@ -18,7 +21,7 @@ const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, sk
return ( return (
<Dialog onClose={handleOnClose} open={isOpen}> <Dialog onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className="dialog-title"> <DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Request Password Reset</Typography> <Typography variant="h6">{ t('RequestPasswordResetDialog.title') }</Typography>
{handleOnClose ? ( {handleOnClose ? (
<IconButton onClick={handleOnClose}> <IconButton onClick={handleOnClose}>

View file

@ -0,0 +1,5 @@
{
"ResetPasswordDialog": {
"title": "Reset Password"
}
}

View file

@ -5,12 +5,15 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { useTranslation } from 'react-i18next';
import { ResetPasswordForm } from 'forms'; import { ResetPasswordForm } from 'forms';
import './ResetPasswordDialog.css'; import './ResetPasswordDialog.css';
const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName }: any) => { const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName }: any) => {
const { t } = useTranslation();
const handleOnClose = () => { const handleOnClose = () => {
handleClose(); handleClose();
} }
@ -18,7 +21,7 @@ const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName
return ( return (
<Dialog onClose={handleOnClose} open={isOpen}> <Dialog onClose={handleOnClose} open={isOpen}>
<DialogTitle disableTypography className="dialog-title"> <DialogTitle disableTypography className="dialog-title">
<Typography variant="h6">Reset Password</Typography> <Typography variant="h6">{t('ResetPasswordDialog.title')}</Typography>
{handleOnClose ? ( {handleOnClose ? (
<IconButton onClick={handleOnClose}> <IconButton onClick={handleOnClose}>

View file

@ -0,0 +1,10 @@
{
"AccountActivationForm": {
"error": {
"failed": "Account activation failed"
},
"label": {
"activate": "Activate Account"
}
}
}

View file

@ -3,6 +3,7 @@ import React, { useState } from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field } from 'react-final-form'; import { Form, Field } from 'react-final-form';
import { OnChange } from 'react-final-form-listeners'; import { OnChange } from 'react-final-form-listeners';
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
@ -16,6 +17,7 @@ import { ServerTypes } from 'store';
const AccountActivationForm = ({ onSubmit }) => { const AccountActivationForm = ({ onSubmit }) => {
const [errorMessage, setErrorMessage] = useState(false); const [errorMessage, setErrorMessage] = useState(false);
const { t } = useTranslation();
useReduxEffect(() => { useReduxEffect(() => {
setErrorMessage(true); setErrorMessage(true);
@ -33,7 +35,7 @@ const AccountActivationForm = ({ onSubmit }) => {
const errors: any = {}; const errors: any = {};
if (!values.token) { if (!values.token) {
errors.token = 'Required'; errors.token = t('Common.validation.required');
} }
return errors; return errors;
@ -45,17 +47,17 @@ const AccountActivationForm = ({ onSubmit }) => {
return ( return (
<form className="AccountActivationForm" onSubmit={handleSubmit}> <form className="AccountActivationForm" onSubmit={handleSubmit}>
<div className="AccountActivationForm-item"> <div className="AccountActivationForm-item">
<Field label="Token" name="token" component={InputField} /> <Field label={t('Common.label.token')} name="token" component={InputField} />
</div> </div>
{errorMessage && ( {errorMessage && (
<div className="AccountActivationForm-error"> <div className="AccountActivationForm-error">
<Typography color="error">Account activation failed</Typography> <Typography color="error">{ t('AccountActivationForm.error.failed') }</Typography>
</div> </div>
)} )}
<Button className="AccountActivationForm-submit rounded tall" color="primary" variant="contained" type="submit"> <Button className="AccountActivationForm-submit rounded tall" color="primary" variant="contained" type="submit">
Activate Account { t('AccountActivationForm.label.activate') }
</Button> </Button>
</form> </form>
); );

View file

@ -0,0 +1,9 @@
{
"KnownHostForm": {
"help": "Need help adding a new host?",
"label": {
"add": "Add Host",
"find": "Find Host"
}
}
}

View file

@ -2,6 +2,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field } from 'react-final-form' import { Form, Field } from 'react-final-form'
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import AnchorLink from '@material-ui/core/Link'; import AnchorLink from '@material-ui/core/Link';
@ -12,20 +13,21 @@ import './KnownHostForm.css';
const KnownHostForm = ({ host, onRemove, onSubmit }) => { const KnownHostForm = ({ host, onRemove, onSubmit }) => {
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const { t } = useTranslation();
const validate = values => { const validate = values => {
const errors: any = {}; const errors: any = {};
if (!values.name) { if (!values.name) {
errors.name = 'Required' errors.name = t('Common.validation.required');
} }
if (!values.host) { if (!values.host) {
errors.host = 'Required' errors.host = t('Common.validation.required');
} }
if (!values.port) { if (!values.port) {
errors.port = 'Required' errors.port = t('Common.validation.required');
} }
if (Object.keys(errors).length) { if (Object.keys(errors).length) {
@ -54,29 +56,29 @@ const KnownHostForm = ({ host, onRemove, onSubmit }) => {
{({ handleSubmit }) => ( {({ handleSubmit }) => (
<form className="KnownHostForm" onSubmit={handleSubmit}> <form className="KnownHostForm" onSubmit={handleSubmit}>
<div className="KnownHostForm-item"> <div className="KnownHostForm-item">
<Field label="Host Name" name="name" component={InputField} /> <Field label={t('Common.label.hostName')} name="name" component={InputField} />
</div> </div>
<div className="KnownHostForm-item"> <div className="KnownHostForm-item">
<Field label="Host Address" name="host" component={InputField} /> <Field label={t('Common.label.hostAddress')} name="host" component={InputField} />
</div> </div>
<div className="KnownHostForm-item"> <div className="KnownHostForm-item">
<Field label="Port" name="port" type="number" component={InputField} /> <Field label={t('Common.label.port')} name="port" type="number" component={InputField} />
</div> </div>
<Button className="KnownHostForm-submit" color="primary" variant="contained" type="submit"> <Button className="KnownHostForm-submit" color="primary" variant="contained" type="submit">
{host ? 'Save Changes' : 'Add Host' } {host ? t('Common.label.saveChanges') : t('KnownHostForm.label.add') }
</Button> </Button>
<div className="KnownHostForm-actions"> <div className="KnownHostForm-actions">
<div className="KnownHostForm-actions__delete"> <div className="KnownHostForm-actions__delete">
{ host && ( { host && (
<Button color="inherit" onClick={() => !confirmDelete ? setConfirmDelete(true) : onRemove(host)}> <Button color="inherit" onClick={() => !confirmDelete ? setConfirmDelete(true) : onRemove(host)}>
{ !confirmDelete ? 'Delete' : 'Are you sure?' } { !confirmDelete ? t('Common.label.delete') : t('Common.label.confirmSure') }
</Button> </Button>
) } ) }
</div> </div>
<AnchorLink href='https://github.com/Cockatrice/Cockatrice/wiki/Public-Servers' target='_blank'> <AnchorLink href='https://github.com/Cockatrice/Cockatrice/wiki/Public-Servers' target='_blank'>
Need help adding a new host? { t('KnownHostForm.label.find') }
</AnchorLink> </AnchorLink>
</div> </div>
</form> </form>

View file

@ -0,0 +1,11 @@
{
"LoginForm": {
"label": {
"autoConnect": "Auto Connect",
"forgot": "Forgot Password",
"login": "Login",
"savePassword": "Save Password",
"savedPassword": "Saved Password"
}
}
}

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { Form, Field } from 'react-final-form'; import { Form, Field } from 'react-final-form';
import { OnChange } from 'react-final-form-listeners'; import { OnChange } from 'react-final-form-listeners';
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
@ -12,22 +13,24 @@ import { APP_USER } from 'types';
import './LoginForm.css'; import './LoginForm.css';
const PASSWORD_LABEL = 'Password';
const STORED_PASSWORD_LABEL = '* Saved Password *';
const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginFormProps) => { const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginFormProps) => {
const { t } = useTranslation();
const PASSWORD_LABEL = t('Common.label.password');
const STORED_PASSWORD_LABEL = `* ${t('LoginForm.label.savedPassword')} *`;
const [host, setHost] = useState(null); const [host, setHost] = useState(null);
const [passwordLabel, setPasswordLabel] = useState(PASSWORD_LABEL); const [passwordLabel, setPasswordLabel] = useState(PASSWORD_LABEL);
const [autoConnect, setAutoConnect] = useAutoConnect(); const [autoConnect, setAutoConnect] = useAutoConnect();
const validate = values => { const validate = values => {
const errors: any = {}; const errors: any = {};
if (!values.userName) { if (!values.userName) {
errors.userName = 'Required'; errors.userName = t('Common.validation.required');
} }
if (!values.selectedHost) { if (!values.selectedHost) {
errors.selectedHost = 'Required'; errors.selectedHost = t('Common.validation.required');
} }
return errors; return errors;
@ -112,7 +115,7 @@ const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginForm
<form className='loginForm' onSubmit={handleSubmit}> <form className='loginForm' onSubmit={handleSubmit}>
<div className='loginForm-items'> <div className='loginForm-items'>
<div className='loginForm-item'> <div className='loginForm-item'>
<Field label='Username' name='userName' component={InputField} autoComplete='username' /> <Field label={t('Common.label.username')} name='userName' component={InputField} autoComplete='username' />
<OnChange name="userName">{onUserNameChange}</OnChange> <OnChange name="userName">{onUserNameChange}</OnChange>
</div> </div>
<div className='loginForm-item'> <div className='loginForm-item'>
@ -127,17 +130,19 @@ const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginForm
/> />
</div> </div>
<div className='loginForm-actions'> <div className='loginForm-actions'>
<Field label='Save Password' name='remember' component={CheckboxField} /> <Field label={t('LoginForm.label.savePassword')} name='remember' component={CheckboxField} />
<OnChange name="remember">{onRememberChange}</OnChange> <OnChange name="remember">{onRememberChange}</OnChange>
<Button color='primary' onClick={onResetPassword}>Forgot Password</Button> <Button color='primary' onClick={onResetPassword}>
{ t('LoginForm.label.forgot') }
</Button>
</div> </div>
<div className='loginForm-item'> <div className='loginForm-item'>
<Field name='selectedHost' component={KnownHosts} /> <Field name='selectedHost' component={KnownHosts} />
<OnChange name="selectedHost">{setHost}</OnChange> <OnChange name="selectedHost">{setHost}</OnChange>
</div> </div>
<div className='loginForm-actions'> <div className='loginForm-actions'>
<Field label='Auto Connect' name='autoConnect' component={CheckboxField} /> <Field label={t('LoginForm.label.autoConnect')} name='autoConnect' component={CheckboxField} />
<OnChange name="autoConnect">{onAutoConnectChange}</OnChange> <OnChange name="autoConnect">{onAutoConnectChange}</OnChange>
</div> </div>
</div> </div>
@ -148,7 +153,7 @@ const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginForm
type='submit' type='submit'
disabled={disableSubmitButton} disabled={disableSubmitButton}
> >
Login { t('LoginForm.label.login') }
</Button> </Button>
</form> </form>
) )

View file

@ -0,0 +1,10 @@
{
"RegisterForm": {
"label": {
"register": "Register"
},
"toast": {
"registerSuccess": "Registration Successful!"
}
}
}

View file

@ -1,7 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { Form, Field } from 'react-final-form'; import { Form, Field } from 'react-final-form';
import { OnChange } from 'react-final-form-listeners'; import { OnChange } from 'react-final-form-listeners';
import setFieldTouched from 'final-form-set-field-touched' import setFieldTouched from 'final-form-set-field-touched';
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
@ -14,12 +15,13 @@ import './RegisterForm.css';
import { useToast } from 'components/Toast'; import { useToast } from 'components/Toast';
const RegisterForm = ({ onSubmit }: RegisterFormProps) => { const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
const { t } = useTranslation();
const [emailRequired, setEmailRequired] = useState(false); const [emailRequired, setEmailRequired] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [emailError, setEmailError] = useState(null); const [emailError, setEmailError] = useState(null);
const [passwordError, setPasswordError] = useState(null); const [passwordError, setPasswordError] = useState(null);
const [userNameError, setUserNameError] = useState(null); const [userNameError, setUserNameError] = useState(null);
const { openToast } = useToast({ key: 'registration-success', children: 'Registration Successful!' }) const { openToast } = useToast({ key: 'registration-success', children: t('RegisterForm.toast.registerSuccess') })
const onHostChange = (host) => setEmailRequired(false); const onHostChange = (host) => setEmailRequired(false);
const onEmailChange = () => emailError && setEmailError(null); const onEmailChange = () => emailError && setEmailError(null);
@ -64,31 +66,31 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
const errors: any = {}; const errors: any = {};
if (!values.userName) { if (!values.userName) {
errors.userName = 'Required'; errors.userName = t('Common.validation.required');
} else if (userNameError) { } else if (userNameError) {
errors.userName = userNameError; errors.userName = userNameError;
} }
if (!values.password) { if (!values.password) {
errors.password = 'Required'; errors.password = t('Common.validation.required');
} else if (values.password.length < 8) { } else if (values.password.length < 8) {
errors.password = 'Minimum of 8 characters required'; errors.password = t('Common.validation.minChars', { count: 8 });
} else if (passwordError) { } else if (passwordError) {
errors.password = passwordError; errors.password = passwordError;
} }
if (!values.passwordConfirm) { if (!values.passwordConfirm) {
errors.passwordConfirm = 'Required'; errors.passwordConfirm = t('Common.validation.required');
} else if (values.password !== values.passwordConfirm) { } else if (values.password !== values.passwordConfirm) {
errors.passwordConfirm = 'Passwords don\'t match' errors.passwordConfirm = t('Common.validation.passwordsMustMatch');
} }
if (!values.selectedHost) { if (!values.selectedHost) {
errors.selectedHost = 'Required'; errors.selectedHost = t('Common.validation.required');
} }
if (emailRequired && !values.email) { if (emailRequired && !values.email) {
errors.email = 'Required'; errors.email = t('Common.validation.required');
} else if (emailError) { } else if (emailError) {
errors.email = emailError; errors.email = emailError;
} }
@ -111,16 +113,22 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
<form className="RegisterForm" onSubmit={handleSubmit}> <form className="RegisterForm" onSubmit={handleSubmit}>
<div className="RegisterForm-column"> <div className="RegisterForm-column">
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field label="Player Name" name="userName" component={InputField} autoComplete="username" /> <Field label={t('Common.label.username')} name="userName" component={InputField} autoComplete="username" />
<OnChange name="userName">{onUserNameChange}</OnChange> <OnChange name="userName">{onUserNameChange}</OnChange>
</div> </div>
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field label="Password" name="password" type="password" component={InputField} autoComplete='new-password' /> <Field
label={t('Common.label.password')}
name="password"
type="password"
component={InputField}
autoComplete='new-password'
/>
<OnChange name="password">{onPasswordChange}</OnChange> <OnChange name="password">{onPasswordChange}</OnChange>
</div> </div>
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field <Field
label="Confirm Password" label={t('Common.label.confirmPassword')}
name="passwordConfirm" name="passwordConfirm"
type="password" type="password"
component={InputField} component={InputField}
@ -134,17 +142,17 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
</div> </div>
<div className="RegisterForm-column" > <div className="RegisterForm-column" >
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field label="Real Name" name="realName" component={InputField} /> <Field label={t('Common.label.realName')} name="realName" component={InputField} />
</div> </div>
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field label="Email" name="email" type="email" component={InputField} /> <Field label={t('Common.label.email')} name="email" type="email" component={InputField} />
<OnChange name="email">{onEmailChange}</OnChange> <OnChange name="email">{onEmailChange}</OnChange>
</div> </div>
<div className="RegisterForm-item"> <div className="RegisterForm-item">
<Field label="Country" name="country" component={CountryDropdown} /> <Field label={t('Common.label.country')} name="country" component={CountryDropdown} />
</div> </div>
<Button className="RegisterForm-submit tall" color="primary" variant="contained" type="submit"> <Button className="RegisterForm-submit tall" color="primary" variant="contained" type="submit">
Register { t('RegisterForm.label.register') }
</Button> </Button>
</div> </div>
</form> </form>

View file

@ -0,0 +1,8 @@
{
"RequestPasswordResetForm": {
"error": "Request password reset failed",
"mfaEnabled": "Server has multi-factor authentication enabled",
"request": "Request Reset Token",
"skipRequest": "I already have a reset token"
}
}

View file

@ -3,6 +3,7 @@ import React, { useState } from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field } from 'react-final-form'; import { Form, Field } from 'react-final-form';
import { OnChange } from 'react-final-form-listeners'; import { OnChange } from 'react-final-form-listeners';
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
@ -17,6 +18,7 @@ import { ServerTypes } from 'store';
const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => { const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
const [errorMessage, setErrorMessage] = useState(false); const [errorMessage, setErrorMessage] = useState(false);
const [isMFA, setIsMFA] = useState(false); const [isMFA, setIsMFA] = useState(false);
const { t } = useTranslation();
useReduxEffect(() => { useReduxEffect(() => {
setErrorMessage(true); setErrorMessage(true);
@ -39,13 +41,13 @@ const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
const errors: any = {}; const errors: any = {};
if (!values.userName) { if (!values.userName) {
errors.userName = 'Required'; errors.userName = t('Common.validation.required');
} }
if (isMFA && !values.email) { if (isMFA && !values.email) {
errors.email = 'Required'; errors.email = t('Common.validation.required');
} }
if (!values.selectedHost) { if (!values.selectedHost) {
errors.selectedHost = 'Required'; errors.selectedHost = t('Common.validation.required');
} }
return errors; return errors;
@ -63,12 +65,12 @@ const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
<form className="RequestPasswordResetForm" onSubmit={handleSubmit}> <form className="RequestPasswordResetForm" onSubmit={handleSubmit}>
<div className="RequestPasswordResetForm-items"> <div className="RequestPasswordResetForm-items">
<div className="RequestPasswordResetForm-item"> <div className="RequestPasswordResetForm-item">
<Field label="Username" name="userName" component={InputField} autoComplete="username" disabled={isMFA} /> <Field label={t('Common.label.username')} name="userName" component={InputField} autoComplete="username" disabled={isMFA} />
</div> </div>
{isMFA ? ( {isMFA ? (
<div className="RequestPasswordResetForm-item"> <div className="RequestPasswordResetForm-item">
<Field label="Email" name="email" type="email" component={InputField} autoComplete="email" /> <Field label={t('Common.label.email')} name="email" type="email" component={InputField} autoComplete="email" />
<div>Server has multi-factor authentication enabled</div> <div>{ t('RequestPasswordResetForm.mfaEnabled') }</div>
</div> </div>
) : null} ) : null}
<div className="RequestPasswordResetForm-item selectedHost"> <div className="RequestPasswordResetForm-item selectedHost">
@ -78,18 +80,18 @@ const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => {
{errorMessage && ( {errorMessage && (
<div className="RequestPasswordResetForm-item"> <div className="RequestPasswordResetForm-item">
<Typography color="error">Request password reset failed</Typography> <Typography color="error">{ t('RequestPasswordResetForm.error') }</Typography>
</div> </div>
)} )}
</div> </div>
<Button className="RequestPasswordResetForm-submit rounded tall" color="primary" variant="contained" type="submit"> <Button className="RequestPasswordResetForm-submit rounded tall" color="primary" variant="contained" type="submit">
Request Reset Token { t('RequestPasswordResetForm.request') }
</Button> </Button>
<div> <div>
<Button color="primary" onClick={() => skipTokenRequest(form.getState().values.userName)}> <Button color="primary" onClick={() => skipTokenRequest(form.getState().values.userName)}>
I already have a reset token { t('RequestPasswordResetForm.skipRequest') }
</Button> </Button>
</div> </div>
</form> </form>

View file

@ -0,0 +1,8 @@
{
"ResetPasswordForm": {
"error": "Password reset failed",
"label": {
"reset": "Reset Password"
}
}
}

View file

@ -2,7 +2,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field } from 'react-final-form' import { Form, Field } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners' import { OnChange } from 'react-final-form-listeners';
import { useTranslation } from 'react-i18next';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
@ -16,6 +17,7 @@ import { ServerTypes } from '../../store';
const ResetPasswordForm = ({ onSubmit, userName }) => { const ResetPasswordForm = ({ onSubmit, userName }) => {
const [errorMessage, setErrorMessage] = useState(false); const [errorMessage, setErrorMessage] = useState(false);
const { t } = useTranslation();
useReduxEffect(() => { useReduxEffect(() => {
setErrorMessage(true); setErrorMessage(true);
@ -25,25 +27,25 @@ const ResetPasswordForm = ({ onSubmit, userName }) => {
const errors: any = {}; const errors: any = {};
if (!values.userName) { if (!values.userName) {
errors.userName = 'Required'; errors.userName = t('Common.validation.required');
} }
if (!values.token) { if (!values.token) {
errors.token = 'Required'; errors.token = t('Common.validation.required');
} }
if (!values.newPassword) { if (!values.newPassword) {
errors.newPassword = 'Required'; errors.newPassword = t('Common.validation.required');
} else if (values.newPassword.length < 8) { } else if (values.newPassword.length < 8) {
errors.newPassword = 'Minimum of 8 characters required'; errors.newPassword = t('Common.validation.minChars', { count: 8 });
} }
if (!values.passwordAgain) { if (!values.passwordAgain) {
errors.passwordAgain = 'Required'; errors.passwordAgain = t('Common.validation.required');
} else if (values.newPassword !== values.passwordAgain) { } else if (values.newPassword !== values.passwordAgain) {
errors.passwordAgain = 'Passwords don\'t match' errors.passwordAgain = t('Common.validation.passwordsMustMatch');
} }
if (!values.selectedHost) { if (!values.selectedHost) {
errors.selectedHost = 'Required'; errors.selectedHost = t('Common.validation.required');
} }
return errors; return errors;
@ -62,16 +64,34 @@ const ResetPasswordForm = ({ onSubmit, userName }) => {
<form className='ResetPasswordForm' onSubmit={handleSubmit}> <form className='ResetPasswordForm' onSubmit={handleSubmit}>
<div className='ResetPasswordForm-items'> <div className='ResetPasswordForm-items'>
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Field label='Username' name='userName' component={InputField} autoComplete='username' disabled={!!userName} /> <Field
label={t('Common.label.username')}
name='userName'
component={InputField}
autoComplete='username'
disabled={!!userName}
/>
</div> </div>
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Field label='Token' name='token' component={InputField} /> <Field label={t('Common.label.token')} name='token' component={InputField} />
</div> </div>
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Field label='Password' name='newPassword' type='password' component={InputField} autoComplete='new-password' /> <Field
label={t('Common.label.password')}
name='newPassword'
type='password'
component={InputField}
autoComplete='new-password'
/>
</div> </div>
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Field label='Password Again' name='passwordAgain' type='password' component={InputField} autoComplete='new-password' /> <Field
label={t('Common.label.passwordAgain')}
name='passwordAgain'
type='password'
component={InputField}
autoComplete='new-password'
/>
</div> </div>
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Field name='selectedHost' component={KnownHosts} disabled /> <Field name='selectedHost' component={KnownHosts} disabled />
@ -79,12 +99,12 @@ const ResetPasswordForm = ({ onSubmit, userName }) => {
{errorMessage && ( {errorMessage && (
<div className='ResetPasswordForm-item'> <div className='ResetPasswordForm-item'>
<Typography color="error">Password reset failed</Typography> <Typography color="error">{ t('ResetPasswordForm.error') }</Typography>
</div> </div>
)} )}
</div> </div>
<Button className='ResetPasswordForm-submit rounded tall' color='primary' variant='contained' type='submit'> <Button className='ResetPasswordForm-submit rounded tall' color='primary' variant='contained' type='submit'>
Reset Password { t('ResetPasswordForm.label.reset') }
</Button> </Button>
</form> </form>
)} )}

View file

@ -1 +1 @@
{"Common":{"language":"Translate into English.","disconnect":"Disconnect"},"LoginContainer":{"title":"Login","subtitle":"A cross-platform virtual tabletop for multiplayer card games."}} {"Common":{"language":"Translate into English.","disconnect":"Disconnect","label":{"confirmPassword":"Confirm Password","confirmSure":"Are you sure?","country":"Country","delete":"Delete","email":"Email","hostName":"Host Name","hostAddress":"Host Address","password":"Password","passwordAgain":"Password Again","port":"Port","realName":"Real Name","saveChanges":"Save Changes","token":"Token","username":"Username"},"validation":{"minChars":"Minimum of {count} {count, plural, one {character} other {characters}} required","passwordsMustMatch":"Passwords don't match","required":"Required"}},"KnownHosts":{"label":"Host","add":"Add new host","toast":"Host successfully {mode, select, created {created} deleted {deleted} other {edited}}."},"InitializeContainer":{"title":"DID YOU KNOW","subtitle":"<1>Cockatrice is run by volunteers</1><1>that love card games!</1>"},"LoginContainer":{"header":{"title":"Login","subtitle":"A cross-platform virtual tabletop for multiplayer card games."},"footer":{"registerPrompt":"Not registered yet?","registerAction":"Create an account","credit":"Cockatrice is an open source project","version":"Version"},"content":{"subtitle1":"Play multiplayer card games online.","subtitle2":"Cross-platform virtual tabletop for multiplayer card games. Forever free."},"toasts":{"passwordResetSuccessToast":"Password Reset Successfully","accountActivationSuccess":"Account Activated Successfully"}},"UnsupportedContainer":{"title":"Unsupported Browser","subtitle1":"Please update your browser and/or check your permissions.","subtitle2":"Note: Private browsing causes some browsers to disable certain permissions or features."},"AccountActivationDialog":{"title":"Account Activation","subtitle1":"Your account has not been activated yet.","subtitle2":"You need to provide the activation token received in the activation email."},"KnownHostDialog":{"title":"{mode, select, edit {Edit} other {Add}} Known Host","subtitle":"Adding a new host allows you to connect to different servers. Enter the details below to your host list."},"RegistrationDialog":{"title":"Create New Account"},"RequestPasswordResetDialog":{"title":"Request Password Reset"},"ResetPasswordDialog":{"title":"Reset Password"},"AccountActivationForm":{"error":{"failed":"Account activation failed"},"label":{"activate":"Activate Account"}},"KnownHostForm":{"help":"Need help adding a new host?","label":{"add":"Add Host","find":"Find Host"}},"LoginForm":{"label":{"autoConnect":"Auto Connect","forgot":"Forgot Password","login":"Login","savePassword":"Save Password","savedPassword":"Saved Password"}},"RegisterForm":{"label":{"register":"Register"},"toast":{"registerSuccess":"Registration Successful!"}},"RequestPasswordResetForm":{"error":"Request password reset failed","mfaEnabled":"Server has multi-factor authentication enabled","request":"Request Reset Token","skipRequest":"I already have a reset token"},"ResetPasswordForm":{"error":"Password reset failed","label":{"reset":"Reset Password"}}}

View file

@ -1,5 +1,6 @@
import i18n from 'i18next'; import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import ICU from 'i18next-icu';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { Language } from 'types'; import { Language } from 'types';
@ -10,6 +11,7 @@ import I18nBackend from './i18n-backend';
import translation from './i18n-default.json'; import translation from './i18n-default.json';
i18n i18n
.use(ICU)
.use(I18nBackend) .use(I18nBackend)
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
@ -24,7 +26,7 @@ i18n
interpolation: { interpolation: {
// not needed for react as it escapes by default // not needed for react as it escapes by default
escapeValue: false, escapeValue: false,
}, }
}); });
export default i18n; export default i18n;