Merge pull request #5452 from SimonBrandner/feature-multi-language-spell-check
Add multi language spell checkpull/21833/head
commit
a5ee029c62
|
@ -213,6 +213,7 @@
|
||||||
@import "./views/settings/_DevicesPanel.scss";
|
@import "./views/settings/_DevicesPanel.scss";
|
||||||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||||
@import "./views/settings/_EmailAddresses.scss";
|
@import "./views/settings/_EmailAddresses.scss";
|
||||||
|
@import "./views/settings/_SpellCheckLanguages.scss";
|
||||||
@import "./views/settings/_IntegrationManager.scss";
|
@import "./views/settings/_IntegrationManager.scss";
|
||||||
@import "./views/settings/_Notifications.scss";
|
@import "./views/settings/_Notifications.scss";
|
||||||
@import "./views/settings/_PhoneNumbers.scss";
|
@import "./views/settings/_PhoneNumbers.scss";
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ExistingSpellCheckLanguage {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ExistingSpellCheckLanguage_language {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpellCheckLanguages {
|
||||||
|
@mixin mx_Settings_fullWidthField;
|
||||||
|
}
|
|
@ -131,6 +131,14 @@ export default abstract class BasePlatform {
|
||||||
hideUpdateToast();
|
hideUpdateToast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if platform supports multi-language
|
||||||
|
* spell-checking, otherwise false.
|
||||||
|
*/
|
||||||
|
supportsMultiLanguageSpellCheck(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the platform supports displaying
|
* Returns true if the platform supports displaying
|
||||||
* notifications, otherwise false.
|
* notifications, otherwise false.
|
||||||
|
@ -240,6 +248,16 @@ export default abstract class BasePlatform {
|
||||||
|
|
||||||
setLanguage(preferredLangs: string[]) {}
|
setLanguage(preferredLangs: string[]) {}
|
||||||
|
|
||||||
|
setSpellCheckLanguages(preferredLangs: string[]) {}
|
||||||
|
|
||||||
|
getSpellCheckLanguages(): Promise<string[]> | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableSpellCheckLanguages(): Promise<string[]> | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.hash = fragmentAfterLogin || "";
|
url.hash = fragmentAfterLogin || "";
|
||||||
|
|
|
@ -100,10 +100,10 @@ export default class LanguageDropdown extends React.Component {
|
||||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||||
let value = null;
|
let value = null;
|
||||||
if (language) {
|
if (language) {
|
||||||
value = this.props.value || language;
|
value = this.props.value || language;
|
||||||
} else {
|
} else {
|
||||||
language = navigator.language || navigator.userLanguage;
|
language = navigator.language || navigator.userLanguage;
|
||||||
value = this.props.value || language;
|
value = this.props.value || language;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Dropdown
|
return <Dropdown
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Dropdown from "../../views/elements/Dropdown"
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
function languageMatchesSearchQuery(query, language) {
|
||||||
|
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||||
|
if (language.value.toUpperCase() === query.toUpperCase()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpellCheckLanguagesDropdownIProps {
|
||||||
|
className: string,
|
||||||
|
value: string,
|
||||||
|
onOptionChange(language: string),
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpellCheckLanguagesDropdownIState {
|
||||||
|
searchQuery: string,
|
||||||
|
languages: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
|
||||||
|
SpellCheckLanguagesDropdownIState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onSearchChange = this._onSearchChange.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchQuery: '',
|
||||||
|
languages: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const plaf = PlatformPeg.get();
|
||||||
|
if (plaf) {
|
||||||
|
plaf.getAvailableSpellCheckLanguages().then((languages) => {
|
||||||
|
languages.sort(function(a, b) {
|
||||||
|
if (a < b) return -1;
|
||||||
|
if (a > b) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
const langs = [];
|
||||||
|
languages.forEach((language) => {
|
||||||
|
langs.push({
|
||||||
|
label: language,
|
||||||
|
value: language,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.setState({languages: langs});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({languages: ['en']});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchChange(search) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.languages === null) {
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayedLanguages;
|
||||||
|
if (this.state.searchQuery) {
|
||||||
|
displayedLanguages = this.state.languages.filter((lang) => {
|
||||||
|
return languageMatchesSearchQuery(this.state.searchQuery, lang);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
displayedLanguages = this.state.languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = displayedLanguages.map((language) => {
|
||||||
|
return <div key={language.value}>
|
||||||
|
{ language.label }
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// default value here too, otherwise we need to handle null / undefined;
|
||||||
|
// values between mounting and the initial value propgating
|
||||||
|
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||||
|
let value = null;
|
||||||
|
if (language) {
|
||||||
|
value = this.props.value || language;
|
||||||
|
} else {
|
||||||
|
language = navigator.language || navigator.userLanguage;
|
||||||
|
value = this.props.value || language;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Dropdown
|
||||||
|
id="mx_LanguageDropdown"
|
||||||
|
className={this.props.className}
|
||||||
|
onOptionChange={this.props.onOptionChange}
|
||||||
|
onSearchChange={this._onSearchChange}
|
||||||
|
searchEnabled={true}
|
||||||
|
value={value}
|
||||||
|
label={_t("Language Dropdown")}>
|
||||||
|
{ options }
|
||||||
|
</Dropdown>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
|
||||||
|
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
|
||||||
|
interface ExistingSpellCheckLanguageIProps {
|
||||||
|
language: string,
|
||||||
|
onRemoved(language: string),
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpellCheckLanguagesIProps {
|
||||||
|
languages: Array<string>,
|
||||||
|
onLanguagesChange(languages: Array<string>),
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpellCheckLanguagesIState {
|
||||||
|
newLanguage: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||||
|
_onRemove = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
return this.props.onRemoved(this.props.language);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="mx_ExistingSpellCheckLanguage">
|
||||||
|
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
|
||||||
|
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||||
|
{_t("Remove")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
newLanguage: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRemoved = (language) => {
|
||||||
|
const languages = this.props.languages.filter((e) => e !== language);
|
||||||
|
this.props.onLanguagesChange(languages);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onAddClick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const language = this.state.newLanguage;
|
||||||
|
|
||||||
|
if (!language) return;
|
||||||
|
if (this.props.languages.includes(language)) return;
|
||||||
|
|
||||||
|
this.props.languages.push(language)
|
||||||
|
this.props.onLanguagesChange(this.props.languages);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onNewLanguageChange = (language: string) => {
|
||||||
|
if (this.state.newLanguage === language) return;
|
||||||
|
this.setState({newLanguage: language});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
||||||
|
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addButton = (
|
||||||
|
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||||
|
{_t("Add")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_SpellCheckLanguages">
|
||||||
|
{existingSpellCheckLanguages}
|
||||||
|
<form onSubmit={this._onAddClick} noValidate={true}>
|
||||||
|
<SpellCheckLanguagesDropdown
|
||||||
|
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||||
|
value={this.state.newLanguage}
|
||||||
|
onOptionChange={this._onNewLanguageChange} />
|
||||||
|
{addButton}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
|
||||||
import * as languageHandler from "../../../../../languageHandler";
|
import * as languageHandler from "../../../../../languageHandler";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||||
|
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
language: languageHandler.getCurrentLanguage(),
|
language: languageHandler.getCurrentLanguage(),
|
||||||
|
spellCheckLanguages: [],
|
||||||
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
||||||
serverSupportsSeparateAddAndBind: null,
|
serverSupportsSeparateAddAndBind: null,
|
||||||
idServerHasUnsignedTerms: false,
|
idServerHasUnsignedTerms: false,
|
||||||
|
@ -85,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
this._getThreepidState();
|
this._getThreepidState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const plaf = PlatformPeg.get();
|
||||||
|
if (plaf) {
|
||||||
|
this.setState({
|
||||||
|
spellCheckLanguages: await plaf.getSpellCheckLanguages(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
@ -182,6 +193,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
PlatformPeg.get().reload();
|
PlatformPeg.get().reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_onSpellCheckLanguagesChange = (languages) => {
|
||||||
|
this.setState({spellCheckLanguages: languages});
|
||||||
|
|
||||||
|
const plaf = PlatformPeg.get();
|
||||||
|
if (plaf) {
|
||||||
|
plaf.setSpellCheckLanguages(languages);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_onPasswordChangeError = (err) => {
|
_onPasswordChangeError = (err) => {
|
||||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||||
let errMsg = err.error || "";
|
let errMsg = err.error || "";
|
||||||
|
@ -303,6 +323,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderSpellCheckSection() {
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
|
||||||
|
<SpellCheckSettings languages={this.state.spellCheckLanguages}
|
||||||
|
onLanguagesChange={this._onSpellCheckLanguagesChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_renderDiscoverySection() {
|
_renderDiscoverySection() {
|
||||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||||
|
|
||||||
|
@ -381,6 +411,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const plaf = PlatformPeg.get();
|
||||||
|
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||||
|
|
||||||
const discoWarning = this.state.requiredPolicyInfo.hasTerms
|
const discoWarning = this.state.requiredPolicyInfo.hasTerms
|
||||||
? <img className='mx_GeneralUserSettingsTab_warningIcon'
|
? <img className='mx_GeneralUserSettingsTab_warningIcon'
|
||||||
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
|
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
|
||||||
|
@ -409,6 +442,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
{this._renderProfileSection()}
|
{this._renderProfileSection()}
|
||||||
{this._renderAccountSection()}
|
{this._renderAccountSection()}
|
||||||
{this._renderLanguageSection()}
|
{this._renderLanguageSection()}
|
||||||
|
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
|
||||||
{ discoverySection }
|
{ discoverySection }
|
||||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||||
{ accountManagementSection }
|
{ accountManagementSection }
|
||||||
|
|
|
@ -1151,6 +1151,7 @@
|
||||||
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
|
"Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
|
||||||
"Manage integrations": "Manage integrations",
|
"Manage integrations": "Manage integrations",
|
||||||
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
|
"Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
|
||||||
|
"Add": "Add",
|
||||||
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
|
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
|
||||||
"Checking for an update...": "Checking for an update...",
|
"Checking for an update...": "Checking for an update...",
|
||||||
"No update available.": "No update available.",
|
"No update available.": "No update available.",
|
||||||
|
@ -1182,6 +1183,7 @@
|
||||||
"Set a new account password...": "Set a new account password...",
|
"Set a new account password...": "Set a new account password...",
|
||||||
"Account": "Account",
|
"Account": "Account",
|
||||||
"Language and region": "Language and region",
|
"Language and region": "Language and region",
|
||||||
|
"Spell check dictionaries": "Spell check dictionaries",
|
||||||
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
||||||
"Account management": "Account management",
|
"Account management": "Account management",
|
||||||
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
|
||||||
|
@ -1379,7 +1381,6 @@
|
||||||
"Invalid Email Address": "Invalid Email Address",
|
"Invalid Email Address": "Invalid Email Address",
|
||||||
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
|
||||||
"Unable to add email address": "Unable to add email address",
|
"Unable to add email address": "Unable to add email address",
|
||||||
"Add": "Add",
|
|
||||||
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
|
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
|
||||||
"Email Address": "Email Address",
|
"Email Address": "Email Address",
|
||||||
"Remove %(phone)s?": "Remove %(phone)s?",
|
"Remove %(phone)s?": "Remove %(phone)s?",
|
||||||
|
|
Loading…
Reference in New Issue