Merge pull request #2749 from jryans/auth-field

Use Field component in auth flows
This commit is contained in:
J. Ryan Stinnett 2019-03-06 11:18:24 +00:00 committed by GitHub
commit 8bf5e1d19f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 212 additions and 186 deletions

View file

@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_Login_field {
width: 100%;
box-sizing: border-box;
border-radius: 3px;
border: 1px solid $strong-input-border-color;
font-weight: 300;
font-size: 13px;
padding: 9px;
margin-bottom: 14px;
}
.mx_Login_submit { .mx_Login_submit {
@mixin mx_DialogButton; @mixin mx_DialogButton;
width: 100%; width: 100%;
@ -69,74 +58,24 @@ limitations under the License.
color: $warning-color; color: $warning-color;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
/*
height: 24px;
*/
margin-top: 12px; margin-top: 12px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.mx_Login_type_container { .mx_Login_type_container {
display: flex; display: flex;
margin-bottom: 14px; align-items: center;
color: $authpage-primary-color; color: $authpage-primary-color;
.mx_Field {
margin: 0;
}
} }
.mx_Login_type_label { .mx_Login_type_label {
flex-grow: 1; flex-grow: 1;
line-height: 35px;
} }
.mx_Login_type_dropdown { .mx_Login_type_dropdown {
display: inline-block; min-width: 200px;
min-width: 170px;
align-self: flex-end;
flex: 1 1 auto;
}
.mx_Login_field_prefix {
height: 38px;
padding: 0px 5px;
line-height: 38px;
background-color: #eee;
border: 1px solid #c7c7c7;
border-right: 0px;
border-radius: 3px 0px 0px 3px;
text-align: center;
}
.mx_Login_field_has_prefix {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.mx_Login_phoneSection {
display:flex;
}
.mx_Login_phoneCountry {
margin-bottom: 14px;
/* To override mx_Login_field_prefix */
text-align: left;
padding: 0px;
background-color: $primary-bg-color;
}
.mx_Login_field_prefix .mx_Dropdown_input {
/* To use prefix border instead of dropdown border */
border: 0;
}
.mx_Login_phoneCountry .mx_Dropdown_option {
/* To match height of mx_Login_field */
height: 38px;
line-height: 38px;
}
.mx_Login_phoneCountry .mx_Dropdown_option img {
margin: 3px;
vertical-align: top;
} }

View file

@ -78,16 +78,16 @@ limitations under the License.
margin-bottom: 10px; margin-bottom: 10px;
} }
.mx_AuthBody_fieldRow > * { .mx_AuthBody_fieldRow > .mx_Field {
margin: 0 5px; margin: 0 5px;
flex: 1; flex: 1;
} }
.mx_AuthBody_fieldRow > *:first-child { .mx_AuthBody_fieldRow > .mx_Field:first-child {
margin-left: 0; margin-left: 0;
} }
.mx_AuthBody_fieldRow > *:last-child { .mx_AuthBody_fieldRow > .mx_Field:last-child {
margin-right: 0; margin-right: 0;
} }

View file

@ -17,8 +17,16 @@ limitations under the License.
/* TODO: Consider unifying with general input styles in _light.scss */ /* TODO: Consider unifying with general input styles in _light.scss */
.mx_Field { .mx_Field {
display: flex;
position: relative; position: relative;
margin: 1em 0; margin: 1em 0;
border-radius: 4px;
transition: border-color 0.25s;
border: 1px solid $input-border-color;
}
.mx_Field_prefix {
border-right: 1px solid $input-border-color;
} }
.mx_Field input, .mx_Field input,
@ -27,9 +35,10 @@ limitations under the License.
font-weight: normal; font-weight: normal;
font-family: $font-family; font-family: $font-family;
font-size: 14px; font-size: 14px;
border: none;
// Even without a border here, we still need this avoid overlapping the rounded
// corners on the field above.
border-radius: 4px; border-radius: 4px;
transition: border-color 0.25s;
border: 1px solid $input-border-color;
padding: 8px 9px; padding: 8px 9px;
color: $primary-fg-color; color: $primary-fg-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
@ -55,11 +64,14 @@ limitations under the License.
pointer-events: none; pointer-events: none;
} }
.mx_Field:focus-within {
border-color: $input-focused-border-color;
}
.mx_Field input:focus, .mx_Field input:focus,
.mx_Field select:focus, .mx_Field select:focus,
.mx_Field textarea:focus { .mx_Field textarea:focus {
outline: 0; outline: 0;
border-color: $input-focused-border-color;
} }
.mx_Field input::placeholder, .mx_Field input::placeholder,
@ -99,7 +111,8 @@ limitations under the License.
.mx_Field input:not(:placeholder-shown) + label, .mx_Field input:not(:placeholder-shown) + label,
.mx_Field textarea:focus + label, .mx_Field textarea:focus + label,
.mx_Field textarea:not(:placeholder-shown) + label, .mx_Field textarea:not(:placeholder-shown) + label,
.mx_Field select + label /* Always show a select's label on top to not collide with the value */ { .mx_Field select + label /* Always show a select's label on top to not collide with the value */,
.mx_Field_labelAlwaysTopLeft label {
transition: transition:
font-size 0.25s ease-out 0s, font-size 0.25s ease-out 0s,
color 0.25s ease-out 0s, color 0.25s ease-out 0s,
@ -127,3 +140,14 @@ limitations under the License.
background-color: $field-focused-label-bg-color; background-color: $field-focused-label-bg-color;
color: $greyed-fg-color; color: $greyed-fg-color;
} }
// Customise other components when placed inside a Field
.mx_Field .mx_Dropdown_input {
border: initial;
border-radius: initial;
}
.mx_Field .mx_CountryDropdown {
width: 67px;
}

View file

@ -230,6 +230,8 @@ module.exports = React.createClass({
}, },
renderForgot() { renderForgot() {
const Field = sdk.getComponent('elements.Field');
let errorText = null; let errorText = null;
const err = this.state.errorText || this.props.defaultServerDiscoveryError; const err = this.state.errorText || this.props.defaultServerDiscoveryError;
if (err) { if (err) {
@ -275,23 +277,33 @@ module.exports = React.createClass({
{errorText} {errorText}
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<input className="mx_Login_field" type="text" <Field
id="mx_ForgotPassword_email"
name="reset_email" // define a name so browser's password autofill gets less confused name="reset_email" // define a name so browser's password autofill gets less confused
type="text"
label={_t('Email')}
value={this.state.email} value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
placeholder={_t('Email')} autoFocus /> autoFocus
/>
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<input className="mx_Login_field" type="password" <Field
id="mx_ForgotPassword_password"
name="reset_password" name="reset_password"
type="password"
label={_t('Password')}
value={this.state.password} value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
placeholder={_t('Password')} /> />
<input className="mx_Login_field" type="password" <Field
id="mx_ForgotPassword_passwordConfirm"
name="reset_password_confirm" name="reset_password_confirm"
type="password"
label={_t('Confirm')}
value={this.state.password2} value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
placeholder={_t('Confirm')} /> />
</div> </div>
<span>{_t( <span>{_t(
'A verification email will be sent to your inbox to confirm ' + 'A verification email will be sent to your inbox to confirm ' +

View file

@ -138,7 +138,8 @@ class PasswordLogin extends React.Component {
this.props.onUsernameBlur(ev.target.value); this.props.onUsernameBlur(ev.target.value);
} }
onLoginTypeChange(loginType) { onLoginTypeChange(ev) {
const loginType = ev.target.value;
this.props.onError(null); // send a null error to clear any error messages this.props.onError(null); // send a null error to clear any error messages
this.setState({ this.setState({
loginType: loginType, loginType: loginType,
@ -169,67 +170,70 @@ class PasswordLogin extends React.Component {
} }
renderLoginField(loginType) { renderLoginField(loginType) {
const classes = { const Field = sdk.getComponent('elements.Field');
mx_Login_field: true,
}; const classes = {};
switch (loginType) { switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.error = this.props.loginIncorrect && !this.state.username; classes.error = this.props.loginIncorrect && !this.state.username;
return <input return <Field
className="mx_Login_field" className={classNames(classes)}
ref={(e) => {this._loginField = e;}} id="mx_PasswordLogin_email"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password
key="email_input" key="email_input"
type="text" type="text"
name="username" // make it a little easier for browser's remember-password label={_t("Email")}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder="joe@example.com" placeholder="joe@example.com"
value={this.state.username} value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
autoFocus autoFocus
/>; />;
case PasswordLogin.LOGIN_FIELD_MXID: case PasswordLogin.LOGIN_FIELD_MXID:
classes.error = this.props.loginIncorrect && !this.state.username; classes.error = this.props.loginIncorrect && !this.state.username;
return <input return <Field
className={classNames(classes)} className={classNames(classes)}
ref={(e) => {this._loginField = e;}} id="mx_PasswordLogin_username"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password
key="username_input" key="username_input"
type="text" type="text"
name="username" // make it a little easier for browser's remember-password label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
value={this.state.username}
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur} onBlur={this.onUsernameBlur}
placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
value={this.state.username}
autoFocus autoFocus
/>; />;
case PasswordLogin.LOGIN_FIELD_PHONE: { case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
classes.mx_Login_field_has_prefix = true;
classes.error = this.props.loginIncorrect && !this.state.phoneNumber; classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
return <div className="mx_Login_phoneSection">
<CountryDropdown const phoneCountry = <CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix" value={this.state.phoneCountry}
onOptionChange={this.onPhoneCountryChanged} isSmall={true}
value={this.state.phoneCountry} showPrefix={true}
isSmall={true} onOptionChange={this.onPhoneCountryChanged}
showPrefix={true} />;
/>
<input return <Field
className={classNames(classes)} className={classNames(classes)}
ref={(e) => {this._loginField = e;}} id="mx_PasswordLogin_phoneNumber"
key="phone_input" ref={(e) => { this._loginField = e; }}
type="text" name="phoneNumber"
name="phoneNumber" key="phone_input"
onChange={this.onPhoneNumberChanged} type="text"
onBlur={this.onPhoneNumberBlur} label={_t("Phone")}
placeholder={_t("Mobile phone number")} value={this.state.phoneNumber}
value={this.state.phoneNumber} prefix={phoneCountry}
autoFocus onChange={this.onPhoneNumberChanged}
/> onBlur={this.onPhoneNumberBlur}
</div>; autoFocus
/>;
} }
} }
} }
@ -245,6 +249,8 @@ class PasswordLogin extends React.Component {
} }
render() { render() {
const Field = sdk.getComponent('elements.Field');
let forgotPasswordJsx; let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
@ -286,12 +292,9 @@ class PasswordLogin extends React.Component {
} }
const pwFieldClass = classNames({ const pwFieldClass = classNames({
mx_Login_field: true,
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
}); });
const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType); const loginField = this.renderLoginField(this.state.loginType);
let loginType; let loginType;
@ -299,14 +302,32 @@ class PasswordLogin extends React.Component {
loginType = ( loginType = (
<div className="mx_Login_type_container"> <div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label> <label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown <Field
className="mx_Login_type_dropdown" className="mx_Login_type_dropdown"
id="mx_PasswordLogin_type"
element="select"
value={this.state.loginType} value={this.state.loginType}
onOptionChange={this.onLoginTypeChange}> onChange={this.onLoginTypeChange}
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('Username') }</span> >
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span> <option
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span> key={PasswordLogin.LOGIN_FIELD_MXID}
</Dropdown> value={PasswordLogin.LOGIN_FIELD_MXID}
>
{_t('Username')}
</option>
<option
key={PasswordLogin.LOGIN_FIELD_EMAIL}
value={PasswordLogin.LOGIN_FIELD_EMAIL}
>
{_t('Email address')}
</option>
<option
key={PasswordLogin.LOGIN_FIELD_PHONE}
value={PasswordLogin.LOGIN_FIELD_PHONE}
>
{_t('Phone')}
</option>
</Field>
</div> </div>
); );
} }
@ -318,15 +339,19 @@ class PasswordLogin extends React.Component {
{editLink} {editLink}
</h3> </h3>
<form onSubmit={this.onSubmitForm}> <form onSubmit={this.onSubmitForm}>
{ loginType } {loginType}
{ loginField } {loginField}
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <Field
className={pwFieldClass}
id="mx_PasswordLogin_password"
ref={(e) => { this._passwordField = e; }}
type="password"
name="password" name="password"
value={this.state.password} onChange={this.onPasswordChanged} label={_t('Password')}
placeholder={_t('Password')} value={this.state.password}
onChange={this.onPasswordChanged}
/> />
<br /> {forgotPasswordJsx}
{ forgotPasswordJsx }
<input className="mx_Login_submit" <input className="mx_Login_submit"
type="submit" type="submit"
value={_t('Sign in')} value={_t('Sign in')}

View file

@ -306,6 +306,8 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const Field = sdk.getComponent('elements.Field');
let yourMatrixAccountText = _t('Create your Matrix account'); let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) { if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
@ -338,14 +340,16 @@ module.exports = React.createClass({
_t("Email (optional)"); _t("Email (optional)");
emailSection = ( emailSection = (
<div> <Field
<input type="text" ref="email" className={this._classForField(FIELD_EMAIL)}
placeholder={emailPlaceholder} id="mx_RegistrationForm_email"
defaultValue={this.props.defaultEmail} ref="email"
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} type="text"
onBlur={this.onEmailBlur} label={emailPlaceholder}
value={this.state.email} /> defaultValue={this.props.defaultEmail}
</div> value={this.state.email}
onBlur={this.onEmailBlur}
/>
); );
} }
@ -353,40 +357,33 @@ module.exports = React.createClass({
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection; let phoneSection;
if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) { if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) {
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ? const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
_t("Phone") : _t("Phone") :
_t("Phone (optional)"); _t("Phone (optional)");
phoneSection = ( const phoneCountry = <CountryDropdown
<div className="mx_Login_phoneSection"> value={this.state.phoneCountry}
<CountryDropdown ref="phone_country" isSmall={true}
className="mx_Login_phoneCountry mx_Login_field_prefix" showPrefix={true}
value={this.state.phoneCountry} onOptionChange={this.onPhoneCountryChange}
isSmall={true} />;
showPrefix={true}
onOptionChange={this.onPhoneCountryChange} phoneSection = <Field
/> className={this._classForField(FIELD_PHONE_NUMBER)}
<input type="text" ref="phoneNumber" id="mx_RegistrationForm_phoneNumber"
placeholder={phonePlaceholder} ref="phoneNumber"
defaultValue={this.props.defaultPhoneNumber} type="text"
className={this._classForField( label={phoneLabel}
FIELD_PHONE_NUMBER, defaultValue={this.props.defaultPhoneNumber}
'mx_Login_phoneNumberField', value={this.state.phoneNumber}
'mx_Login_field', prefix={phoneCountry}
'mx_Login_field_has_prefix', onBlur={this.onPhoneNumberBlur}
)} />;
onBlur={this.onPhoneNumberBlur}
value={this.state.phoneNumber}
/>
</div>
);
} }
const registerButton = ( const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} />
); );
const placeholderUsername = _t("Username");
return ( return (
<div> <div>
<h3> <h3>
@ -395,22 +392,36 @@ module.exports = React.createClass({
</h3> </h3>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<input type="text" ref="username" <Field
className={this._classForField(FIELD_USERNAME)}
id="mx_RegistrationForm_username"
ref="username"
type="text"
autoFocus={true} autoFocus={true}
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername} label={_t("Username")}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')} defaultValue={this.props.defaultUsername}
onBlur={this.onUsernameBlur} /> onBlur={this.onUsernameBlur}
/>
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<input type="password" ref="password" <Field
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')} className={this._classForField(FIELD_PASSWORD)}
id="mx_RegistrationForm_password"
ref="password"
type="password"
label={_t("Password")}
defaultValue={this.props.defaultPassword}
onBlur={this.onPasswordBlur} onBlur={this.onPasswordBlur}
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} /> />
<input type="password" ref="passwordConfirm" <Field
placeholder={_t("Confirm")} className={this._classForField(FIELD_PASSWORD_CONFIRM)}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')} id="mx_RegistrationForm_passwordConfirm"
ref="passwordConfirm"
type="password"
label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
onBlur={this.onPasswordConfirmBlur} onBlur={this.onPasswordConfirmBlur}
defaultValue={this.props.defaultPassword} /> />
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
{ emailSection } { emailSection }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class Field extends React.PureComponent { export default class Field extends React.PureComponent {
static propTypes = { static propTypes = {
@ -30,6 +31,8 @@ export default class Field extends React.PureComponent {
label: PropTypes.string, label: PropTypes.string,
// The field's placeholder string. Defaults to the label. // The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string, placeholder: PropTypes.string,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// All other props pass through to the <input>. // All other props pass through to the <input>.
}; };
@ -46,7 +49,7 @@ export default class Field extends React.PureComponent {
} }
render() { render() {
const { element, children, ...inputProps } = this.props; const { element, prefix, children, ...inputProps } = this.props;
const inputElement = element || "input"; const inputElement = element || "input";
@ -57,7 +60,20 @@ export default class Field extends React.PureComponent {
const fieldInput = React.createElement(inputElement, inputProps, children); const fieldInput = React.createElement(inputElement, inputProps, children);
return <div className={`mx_Field mx_Field_${inputElement}`}> let prefixContainer = null;
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
const classes = classNames("mx_Field", `mx_Field_${inputElement}`, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefix,
});
return <div className={classes}>
{prefixContainer}
{fieldInput} {fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label> <label htmlFor={this.props.id}>{this.props.label}</label>
</div>; </div>;

View file

@ -1258,19 +1258,18 @@
"The username field must not be blank.": "The username field must not be blank.", "The username field must not be blank.": "The username field must not be blank.",
"The phone number field must not be blank.": "The phone number field must not be blank.", "The phone number field must not be blank.": "The phone number field must not be blank.",
"The password field must not be blank.": "The password field must not be blank.", "The password field must not be blank.": "The password field must not be blank.",
"Email": "Email",
"Username on %(hs)s": "Username on %(hs)s", "Username on %(hs)s": "Username on %(hs)s",
"Username": "Username", "Username": "Username",
"Mobile phone number": "Mobile phone number", "Phone": "Phone",
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>", "Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
"Sign in to your Matrix account": "Sign in to your Matrix account", "Sign in to your Matrix account": "Sign in to your Matrix account",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Change": "Change", "Change": "Change",
"Sign in with": "Sign in with", "Sign in with": "Sign in with",
"Phone": "Phone",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Create your Matrix account": "Create your Matrix account", "Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Email": "Email",
"Email (optional)": "Email (optional)", "Email (optional)": "Email (optional)",
"Phone (optional)": "Phone (optional)", "Phone (optional)": "Phone (optional)",
"Confirm": "Confirm", "Confirm": "Confirm",