i18n all the things (#41)

This commit is contained in:
albemarle
2019-02-28 10:53:39 -05:00
committed by GitHub
parent a4534551e9
commit 30f158d109
18 changed files with 267 additions and 53 deletions

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
export default class NotFoundPage extends Component {
componentDidMount() {}
@@ -6,8 +7,11 @@ export default class NotFoundPage extends Component {
render() {
return (
<div>
The page you&apos;re looking for is unavailable or there&apos;s an error in the URL.
Please check the URL and try again.
<FormattedMessage
id="profile.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</div>
);
}

View File

@@ -1,16 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { UncontrolledAlert } from 'reactstrap';
import { FormattedMessage } from 'react-intl';
function AgeMessage({ accountURL }) {
return (
<UncontrolledAlert color="info">
<h6>Your profile cannot be shared.</h6>
<p>
To share your profile with other edX learners,
you must confirm that you are over the age of 13.
</p>
<a href={accountURL}>Set your date of birth</a>
<FormattedMessage
id="profile.age.headline"
defaultMessage="Your profile cannot be shared."
description="error message"
tagName="h6"
/>
<FormattedMessage
id="profile.age.details"
defaultMessage="To share your profile with other edX learners, you must confirm that you are over the age of 13."
description="error message"
tagName="p"
/>
<a href={accountURL}>
<FormattedMessage
id="profile.age.set.date"
defaultMessage="Set your date of birth"
description="label on a link to set birthday"
/>
</a>
</UncontrolledAlert>
);
}

View File

@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import messages from './Bio.messages';
// Components
import FormControls from './elements/FormControls';
@@ -43,7 +45,7 @@ class Bio extends React.Component {
render() {
const {
formId, value, visibility, editMode, saveState, error,
formId, value, visibility, editMode, saveState, error, intl,
} = this.props;
return (
@@ -54,7 +56,7 @@ class Bio extends React.Component {
editing: (
<Form onSubmit={this.handleSubmit}>
<FormGroup>
<Label for={formId}>About Me</Label>
<Label for={formId}>{intl.formatMessage(messages['profile.bio.about.me'])}</Label>
<Input
type="textarea"
id={formId}
@@ -77,7 +79,7 @@ class Bio extends React.Component {
editable: (
<React.Fragment>
<EditableItemHeader
content="About Me"
content={intl.formatMessage(messages['profile.bio.about.me'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibility !== null}
@@ -97,7 +99,7 @@ class Bio extends React.Component {
),
static: (
<React.Fragment>
<EditableItemHeader content="About Me" />
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
<p className="lead">{value}</p>
</React.Fragment>
),
@@ -126,6 +128,9 @@ Bio.propTypes = {
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Bio.defaultProps = {
@@ -139,4 +144,4 @@ Bio.defaultProps = {
export default connect(
editableFormSelector,
{},
)(Bio);
)(injectIntl(Bio));

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.bio.about.me': {
id: 'profile.bio.about.me',
defaultMessage: 'About Me',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -1,11 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { Row, Col, Card, CardBody, CardTitle, Button, Form } from 'reactstrap';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import messages from './Certificates.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
@@ -79,7 +81,7 @@ class Certificates extends React.Component {
render() {
const {
formId, visibility, editMode, saveState,
formId, visibility, editMode, saveState, intl,
} = this.props;
return (
@@ -89,7 +91,7 @@ class Certificates extends React.Component {
cases={{
editing: (
<Form onSubmit={this.handleSubmit}>
<EditableItemHeader content="My Certificates" />
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
{this.renderCertificates()}
<FormControls
formId={formId}
@@ -103,7 +105,7 @@ class Certificates extends React.Component {
editable: (
<React.Fragment>
<EditableItemHeader
content="My Certificates"
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibility !== null}
@@ -123,7 +125,7 @@ class Certificates extends React.Component {
),
static: (
<React.Fragment>
<EditableItemHeader content="My Certificates" />
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
{this.renderCertificates()}
</React.Fragment>
),
@@ -153,6 +155,9 @@ Certificates.propTypes = {
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Certificates.defaultProps = {
@@ -165,4 +170,4 @@ Certificates.defaultProps = {
export default connect(
certificatesSelector,
{},
)(Certificates);
)(injectIntl(Certificates));

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.certificates.my.certificates': {
id: 'profile.certificates.my.certificates',
defaultMessage: 'My Certificates',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
// Components
import FormControls from './elements/FormControls';
@@ -95,7 +96,15 @@ class Country extends React.Component {
<h5>{ALL_COUNTRIES[value]}</h5>
</React.Fragment>
),
empty: <EmptyContent onClick={this.handleOpen}>Add location</EmptyContent>,
empty: (
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
id="profile.country.empty"
defaultMessage="Add location"
description="instructions when the user doesn't have a location set"
/>
</EmptyContent>
),
static: (
<React.Fragment>
<EditableItemHeader content="Location" />

View File

@@ -2,6 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormFeedback, FormGroup, Input, Label } from 'reactstrap';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import messages from './Education.messages';
// Components
import FormControls from './elements/FormControls';
@@ -48,7 +51,7 @@ class Education extends React.Component {
render() {
const {
formId, value, visibility, editMode, saveState, error,
formId, value, visibility, editMode, saveState, error, intl,
} = this.props;
return (
@@ -59,7 +62,9 @@ class Education extends React.Component {
editing: (
<Form onSubmit={this.handleSubmit}>
<FormGroup>
<Label for="education">Education</Label>
<Label for="education">
{intl.formatMessage(messages['profile.education.education'])}
</Label>
<Input
type="select"
name={formId}
@@ -86,7 +91,7 @@ class Education extends React.Component {
editable: (
<React.Fragment>
<EditableItemHeader
content="Education"
content={intl.formatMessage(messages['profile.education.education'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibility !== null}
@@ -95,10 +100,18 @@ class Education extends React.Component {
<h5>{EDUCATION[value]}</h5>
</React.Fragment>
),
empty: <EmptyContent onClick={this.handleOpen}>Add education</EmptyContent>,
empty: (
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
id="profile.education.empty"
defaultMessage="Add education"
description="instructions when the user doesn't have their level of education set"
/>
</EmptyContent>
),
static: (
<React.Fragment>
<EditableItemHeader content="Education" />
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
<h5>{EDUCATION[value]}</h5>
</React.Fragment>
),
@@ -127,6 +140,9 @@ Education.propTypes = {
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Education.defaultProps = {
@@ -140,4 +156,4 @@ Education.defaultProps = {
export default connect(
editableFormSelector,
{},
)(Education);
)(injectIntl(Education));

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.education.education': {
id: 'profile.education.education',
defaultMessage: 'Education',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -2,6 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormFeedback, FormGroup, FormText, Input, Label } from 'reactstrap';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import messages from './Name.messages';
// Components
import FormControls from './elements/FormControls';
@@ -45,7 +48,7 @@ class Name extends React.Component {
render() {
const {
formId, value, visibility, editMode, saveState, error,
formId, value, visibility, editMode, saveState, error, intl,
} = this.props;
return (
@@ -59,7 +62,11 @@ class Name extends React.Component {
<Label for="name">Full Name</Label>
<Input type="text" name={formId} value={value} invalid={error != null} onChange={this.handleChange} />
<FormText>
This is the name that appears in your account and on your certificates.
<FormattedMessage
id="profile.name.details"
defaultMessage="This is the name that appears in your account and on your certificates."
description="describes the area for the user to update their name"
/>
</FormText>
<FormFeedback>{error}</FormFeedback>
</FormGroup>
@@ -75,7 +82,7 @@ class Name extends React.Component {
editable: (
<React.Fragment>
<EditableItemHeader
content="Full Name"
content={intl.formatMessage(messages['profile.name.full.name'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibility !== null}
@@ -84,10 +91,18 @@ class Name extends React.Component {
<h5>{value}</h5>
</React.Fragment>
),
empty: <EmptyContent onClick={this.handleOpen}>Add name</EmptyContent>,
empty: (
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
id="profile.name.empty"
defaultMessage="Add name"
description="instructions when the user hasn't entered their name"
/>
</EmptyContent>
),
static: (
<React.Fragment>
<EditableItemHeader content="Full Name" />
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
<h5>{value}</h5>
</React.Fragment>
),
@@ -116,6 +131,9 @@ Name.propTypes = {
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Name.defaultProps = {
@@ -129,4 +147,4 @@ Name.defaultProps = {
export default connect(
editableFormSelector,
{},
)(Name);
)(injectIntl(Name));

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.name.full.name': {
id: 'profile.name.full.name',
defaultMessage: 'Full Name',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -4,6 +4,9 @@ import { Form, Input, FormFeedback } from 'reactstrap';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import messages from './SocialLinks.messages';
// Components
import FormControls from './elements/FormControls';
@@ -73,7 +76,7 @@ class SocialLinks extends React.Component {
render() {
const {
formId, value: values, visibility, editMode, saveState, error,
formId, value: values, visibility, editMode, saveState, error, intl,
} = this.props;
return (
@@ -94,7 +97,7 @@ class SocialLinks extends React.Component {
),
static: (
<React.Fragment>
<EditableItemHeader content="Social Links" />
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
<ul className="list-unstyled">
{values.map(({ platform, social_link: socialLink }) => (
<StaticListItem
@@ -110,7 +113,7 @@ class SocialLinks extends React.Component {
editable: (
<React.Fragment>
<EditableItemHeader
content="Social Links"
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibility !== null}
@@ -131,7 +134,7 @@ class SocialLinks extends React.Component {
),
editing: (
<Form onSubmit={this.handleSubmit}>
<EditableItemHeader content="Social Links" />
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
<ul className="list-unstyled">
{values.map(({ platform, social_link: socialLink }) => (
<EditingListItem
@@ -185,6 +188,9 @@ SocialLinks.propTypes = {
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
SocialLinks.defaultProps = {
@@ -199,7 +205,7 @@ SocialLinks.defaultProps = {
export default connect(
editableFormSelector,
{},
)(SocialLinks);
)(injectIntl(SocialLinks));
function SocialLink({ url, name, platform }) {
return (
@@ -281,7 +287,16 @@ EditingListItem.defaultProps = {
function EmptyListItem({ onClick, name }) {
return (
<li className="mb-4">
<EmptyContent onClick={onClick}>Add {name}</EmptyContent>
<EmptyContent onClick={onClick}>
<FormattedMessage
id="profile.sociallinks.add"
defaultMessage="Add {network}"
values={{
network: { name },
}}
description="{network} is the name of a social network such as Facebook or Twitter"
/>
</EmptyContent>
</li>
);
}

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.sociallinks.social.links': {
id: 'profile.sociallinks.social.links',
defaultMessage: 'Social Links',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -3,25 +3,33 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from 'react-intl';
function EditButton({ onClick, className, style }) {
import messages from './EditButton.messages';
function EditButton({
onClick, className, style, intl,
}) {
return (
<button
className={classNames('btn btn-sm btn-link', className)}
onClick={onClick}
style={style}
>
<FontAwesomeIcon icon={faPencilAlt} /> Edit
<FontAwesomeIcon icon={faPencilAlt} /> {intl.formatMessage(messages['profile.editbutton.edit'])}
</button>
);
}
export default EditButton;
export default injectIntl(EditButton);
EditButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
// i18n
intl: intlShape.isRequired,
};
EditButton.defaultProps = {

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.editbutton.edit': {
id: 'profile.editbutton.edit',
defaultMessage: 'Edit',
description: 'A button label',
},
});
export default messages;

View File

@@ -1,17 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input, Button, Label, Row, Col } from 'reactstrap';
import { injectIntl, intlShape } from 'react-intl';
import messages from './FormControls.messages';
import AsyncActionButton from './AsyncActionButton';
function FormControls({
formId, cancelHandler, changeHandler, visibility, saveState,
formId, cancelHandler, changeHandler, visibility, saveState, intl,
}) {
const visibilityId = `${formId}-visibility`;
return (
<Row className="align-items-center flex-wrap-1 pt-3">
<Col xs="auto" className="d-flex mb-3">
<Label className="flex-shrink-0 d-inline-block mb-0 mr-2" size="sm" for={visibilityId}>
Who can see this:
{intl.formatMessage(messages['profile.formcontrols.who.can.see'])}
</Label>
<span>
<Input
@@ -24,26 +28,26 @@ function FormControls({
onChange={changeHandler}
>
<option key="private" value="private">
Just me
{intl.formatMessage(messages['profile.formcontrols.who.just.me'])}
</option>
<option key="all_users" value="all_users">
Everyone on edX
{intl.formatMessage(messages['profile.formcontrols.who.everyone'])}
</option>
</Input>
</span>
</Col>
<Col xs="auto" className="flex-grow-1 d-flex justify-content-end mb-3">
<Button color="link" onClick={cancelHandler}>
Cancel
{intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
</Button>
<AsyncActionButton
type="submit"
variant={saveState}
labels={{
default: 'Save',
pending: 'Saving',
complete: 'Saved',
error: 'Save Failed',
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
pending: intl.formatMessage(messages['profile.formcontrols.button.saving']),
complete: intl.formatMessage(messages['profile.formcontrols.button.saved']),
error: intl.formatMessage(messages['profile.formcontrols.button.save.failed']),
}}
/>
</Col>
@@ -51,7 +55,7 @@ function FormControls({
);
}
export default FormControls;
export default injectIntl(FormControls);
FormControls.propTypes = {
formId: PropTypes.string.isRequired,
@@ -59,6 +63,9 @@ FormControls.propTypes = {
visibility: PropTypes.oneOf(['private', 'all_users']),
cancelHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
FormControls.defaultProps = {

View File

@@ -0,0 +1,46 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.formcontrols.who.can.see': {
id: 'profile.formcontrols.who.can.see',
defaultMessage: 'Who can see this:',
description: 'What users can see this area?',
},
'profile.formcontrols.who.just.me': {
id: 'profile.formcontrols.who.just.me',
defaultMessage: 'Just me',
description: 'What users can see this area?',
},
'profile.formcontrols.who.everyone': {
id: 'profile.formcontrols.who.everyone',
defaultMessage: 'Everyone on edX',
description: 'What users can see this area?',
},
'profile.formcontrols.button.cancel': {
id: 'profile.formcontrols.button.cancel',
defaultMessage: 'Cancel',
description: 'A button label',
},
'profile.formcontrols.button.save': {
id: 'profile.formcontrols.button.save',
defaultMessage: 'Save',
description: 'A button label',
},
'profile.formcontrols.button.saving': {
id: 'profile.formcontrols.button.saving',
defaultMessage: 'Saving',
description: 'A button label',
},
'profile.formcontrols.button.saved': {
id: 'profile.formcontrols.button.saved',
defaultMessage: 'Saved',
description: 'A button label',
},
'profile.formcontrols.button.save.failed': {
id: 'profile.formcontrols.button.save.failed',
defaultMessage: 'Save Failed',
description: 'A button label',
},
});
export default messages;

View File

@@ -1,4 +1,5 @@
{
"profile.no.certificates": "Aún no tienes ningún certificado.",
"profile.bio.empty": "Añada una breve biografía"
"profile.bio.about.me": "Sobre Mi",
"profile.bio.empty": "Añade una breve biografía"
}