Adds basic internationalization support

This patch adds an I18NProvider component to implement react-intl's
translation mechanisms and replaces text strings in the deployment_plan
components with formatted messages.

The patch also contains scripts to convert extracted messages to `.pot`
files (which can be used with zanata.org) and back. See more details in
the README changes.

More components need to be updated once this patch has landed.

Change-Id: Ida9fc65c65bedf377341220be1d7225d1ae58b2c
Implements: blueprint tripleo-ui-i18n-support-for-js
This commit is contained in:
Florian Fuchs 2016-11-18 15:44:26 +01:00
parent 9f2244f848
commit 57c98c657d
24 changed files with 428 additions and 72 deletions

View File

@ -1,3 +1,9 @@
{
"presets": ["es2015", "stage-0", "react"]
"presets": ["es2015", "stage-0", "react"],
"plugins": [
["react-intl", {
"messagesDir": "./i18n/extracted-messages/",
"enforceDescriptions": false
}]
]
}

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ tripleo-ui-*.tar.gz
.DS_Store
.idea
app.conf
messages.pot
i18n/extracted-messages*

View File

@ -122,3 +122,47 @@ src/js/components/environment_configuration
# Documentation
Use JSDoc docstrings in code to provide source for autogenerated documentation (http://usejsdoc.org/).
# Translation
tripleo-ui uses the react-intl package for translation.
## Adding translateable strings
Strings are prepared for translation using react-intl's `defineMessages` API. Check out `./src/js/components/deployment_plan/` for examples.
## Extracting messages from components
Messages are extracted during the build process (`npm run build`) and stored in the `./i18n/extracted-messages/` folder. These files can be converted into a single `.pot` file with this command:
```
npm run json2pot
```
The resulting file (`./messages.pot`) can be uploaded to http://zanata.org to create/update the translation.
## Using translated `.po` files
The translated language file (`messages.po`) then needs to be converted into one JSON file per language (Japanese in this example):
```
npm run po2json -- messages.po -o ./i18n/locales/ja.json
```
## Adding a new language
The languages are defined in the ./src/js/components/i18n/I18NProvider component. To add another language, import the relevant locale data from the react-intl packages, as well as the JSON containing the translation and add the new language to the MESSAGES constant:
```
import ja from 'react-intl/locale-data/ja';
import jaMessages from '../../../../i18n/locales/ja.json';
const MESSAGES = {
ja: jaMessages.messages
};
```

1
i18n/locales/ja.json Normal file
View File

@ -0,0 +1 @@
{"messages":{"ConfigurePlanStep.editConfigurationLink":"","DeleteStackButton.deleteDeployment":"","DeploymentFailure.deleteDeployment":"","DeploymentSuccess.deleteDeployment":"","DeleteStackButton.deleteConfirmationQuestion":"","DeploymentConfiguration.deploymentConfiguration":"","DeploymentConfiguration.overallSettings":"","DeploymentConfiguration.parameters":"パラメーター","DeploymentConfigurationSummary.loadingCurrentConfiguration":"","DeploymentFailure.requestingDeletion":"","DeploymentProgress.requestingDeletion":"","DeploymentSuccess.requestingDeletion":"","DeploymentPlan.hardwareStepHeader":"","DeploymentPlan.configureRolesStepHeader":"","DeploymentPlan.deploymentConfigurationStepHeader":"","DeploymentPlan.deployStepHeader":"展開する","DeploymentProgress.cancelDeployment":"","DeploymentProgress.deploymentInProgress":"","DeploymentProgress.viewInformation":"","DeployStep.validateAndDeploy":"","HardwareStep.registerNodes":"","NodesAssignment.assignUnassignNodes":"","NodesAssignment.done":"","NoPlans.noPlansAvailable":"","NoPlans.noPlansAvailableMessage":"","NoPlans.createNewPlan":"","PlansDropdown.manageDeployments":"","PlansDropdown.selectDeployment":"","RoleCard.nodesAssigned":"","RoleCard.assignNodes":"","Roles.loadingDeploymentRoles":"","RolesStep.loadingNodes":""}}

View File

@ -71,7 +71,9 @@
"lint": "eslint --max-warnings 0 src",
"start": "webpack-dev-server --progress",
"test": "karma start --single-run",
"test:watch": "karma start"
"test:watch": "karma start",
"json2pot": "rip json2pot ./i18n/extracted-messages/**/*.json -o ./messages.pot",
"po2json": "rip po2json"
},
"repository": {
"type": "git",

View File

@ -1,15 +1,23 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import React from 'react';
import DeploymentConfigurationSummary from './DeploymentConfigurationSummary';
export const ConfigurePlanStep = (props) => {
const messages = defineMessages({
editConfigurationLink: {
id: 'ConfigurePlanStep.editConfigurationLink',
defaultMessage: 'Edit Configurations'
}
});
const ConfigurePlanStep = (props) => {
return (
<div>
<DeploymentConfigurationSummary { ...props } />
&nbsp;
<Link to="/deployment-plan/configuration">
Edit Configuration
<FormattedMessage { ...messages.editConfigurationLink} />
</Link>
</div>
);
@ -21,3 +29,5 @@ ConfigurePlanStep.propTypes = {
planName: React.PropTypes.string.isRequired,
summary: React.PropTypes.string.isRequired
};
export default ConfigurePlanStep;

View File

@ -1,10 +1,22 @@
import { defineMessages, injectIntl } from 'react-intl';
import React from 'react';
import ConfirmationModal from '../ui/ConfirmationModal';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Loader from '../ui/Loader';
export default class DeleteStackButton extends React.Component {
const messages = defineMessages({
deleteDeployment: {
id: 'DeleteStackButton.deleteDeployment',
defaultMessage: 'Delete Deployment'
},
deleteConfirmationQuestion: {
id: 'DeleteStackButton.deleteConfirmationQuestion',
defaultMessage: 'Are you sure you want to delete the stack?'
}
});
class DeleteStackButton extends React.Component {
constructor() {
super();
this.state = {
@ -18,6 +30,8 @@ export default class DeleteStackButton extends React.Component {
}
render() {
const { formatMessage } = this.props.intl;
return (
<div>
<button onClick={() => this.setState({ showDeleteModal: true })}
@ -33,8 +47,8 @@ export default class DeleteStackButton extends React.Component {
</Loader>
</button>
<ConfirmationModal show={this.state.showDeleteModal}
title="Delete Deployment"
question="Are you sure you want to delete the stack?"
title={formatMessage(messages.deleteDeployment)}
question={formatMessage(messages.deleteConfirmationQuestion)}
iconClass="pficon pficon-delete"
confirmActionName="delete"
onConfirm={this.confirmDelete.bind(this)}
@ -49,6 +63,7 @@ DeleteStackButton.propTypes = {
content: React.PropTypes.string.isRequired,
deleteStack: React.PropTypes.func.isRequired,
disabled: React.PropTypes.bool.isRequired,
intl: React.PropTypes.object,
loaded: React.PropTypes.bool.isRequired,
loaderContent: React.PropTypes.string.isRequired,
stack: ImmutablePropTypes.record.isRequired
@ -56,3 +71,5 @@ DeleteStackButton.propTypes = {
DeleteStackButton.defaultProps = {
buttonIconClass: 'pficon pficon-delete'
};
export default injectIntl(DeleteStackButton);

View File

@ -1,5 +1,6 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import DeploymentSuccess from './DeploymentSuccess';
import DeploymentFailure from './DeploymentFailure';
@ -8,11 +9,18 @@ import Link from '../ui/Link';
import Loader from '../ui/Loader';
import { stackStates } from '../../constants/StacksConstants';
export const DeployStep = ({ currentPlan, currentStack, currentStackResources,
currentStackResourcesLoaded, currentStackDeploymentProgress,
deleteStack, deployPlan, fetchStackResource, fetchStackEnvironment,
isRequestingStackDelete, runPostDeploymentValidations,
stacksLoaded }) => {
const messages = defineMessages({
validateAndDeploy: {
id: 'DeployStep.validateAndDeploy',
defaultMessage: 'Validate and Deploy'
}
});
const DeployStep = ({ currentPlan, currentStack, currentStackResources, currentStackResourcesLoaded,
currentStackDeploymentProgress, deleteStack, deployPlan, fetchStackResource,
fetchStackEnvironment, isRequestingStackDelete, runPostDeploymentValidations,
stacksLoaded }) => {
if (!currentStack || currentStack.stack_status === stackStates.DELETE_COMPLETE) {
return (
<Loader loaded={stacksLoaded}>
@ -23,7 +31,8 @@ export const DeployStep = ({ currentPlan, currentStack, currentStackResources,
content="Requesting a deploy..."
component="span"
inline>
<span className="fa fa-cloud-upload"/> Validate and Deploy
<span className="fa fa-cloud-upload"/> <FormattedMessage
{...messages.validateAndDeploy}/>
</Loader>
</Link>
</Loader>
@ -69,3 +78,5 @@ DeployStep.propTypes = {
runPostDeploymentValidations: React.PropTypes.func.isRequired,
stacksLoaded: React.PropTypes.bool.isRequired
};
export default DeployStep;

View File

@ -1,9 +1,25 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import React from 'react';
import NavTab from '../ui/NavTab';
import Modal from '../ui/Modal';
const messages = defineMessages({
deploymentConfiguration: {
id: 'DeploymentConfiguration.deploymentConfiguration',
defaultMessage: 'Deployment Configuration'
},
overallSettings: {
id: 'DeploymentConfiguration.overallSettings',
defaultMessage: 'Overall Settings'
},
parameters: {
id: 'DeploymentConfiguration.parameters',
defaultMessage: 'Parameters'
}
});
export default class DeploymentConfiguration extends React.Component {
render() {
return (
@ -14,12 +30,18 @@ export default class DeploymentConfiguration extends React.Component {
className="close">
<span aria-hidden="true" className="pficon pficon-close"/>
</Link>
<h4 className="modal-title">Deployment Configuration</h4>
<h4 className="modal-title">
<FormattedMessage {...messages.deploymentConfiguration}/>
</h4>
</div>
<ul className="nav nav-tabs">
<NavTab to="/deployment-plan/configuration/environment">Overall Settings</NavTab>
<NavTab to="/deployment-plan/configuration/parameters">Parameters</NavTab>
<NavTab to="/deployment-plan/configuration/environment">
<FormattedMessage {...messages.overallSettings}/>
</NavTab>
<NavTab to="/deployment-plan/configuration/parameters">
<FormattedMessage {...messages.parameters}/>
</NavTab>
</ul>
{React.cloneElement(this.props.children,

View File

@ -1,8 +1,16 @@
import { defineMessages, injectIntl } from 'react-intl';
import React from 'react';
import Loader from '../ui/Loader';
export default class DeploymentConfigurationSummary extends React.Component {
const messages = defineMessages({
loadingCurrentConfiguration: {
id: 'DeploymentConfigurationSummary.loadingCurrentConfiguration',
defaultMessage: 'Loading current Deployment Configuration...'
}
});
class DeploymentConfigurationSummary extends React.Component {
componentDidMount() {
if(this.props.planName) {
this.props.fetchEnvironmentConfiguration(this.props.planName);
@ -18,7 +26,7 @@ export default class DeploymentConfigurationSummary extends React.Component {
render() {
return (
<Loader loaded={this.props.loaded}
content="Loading current Deployment Configuration..."
content={this.props.intl.formatMessage(messages.loadingCurrentConfiguration)}
component="span"
inline>
<span>{this.props.summary}</span>
@ -28,8 +36,11 @@ export default class DeploymentConfigurationSummary extends React.Component {
}
DeploymentConfigurationSummary.propTypes = {
fetchEnvironmentConfiguration: React.PropTypes.func.isRequired,
intl: React.PropTypes.object,
isFetching: React.PropTypes.bool.isRequired,
loaded: React.PropTypes.bool.isRequired,
planName: React.PropTypes.string,
summary: React.PropTypes.string.isRequired
};
export default injectIntl(DeploymentConfigurationSummary);

View File

@ -1,3 +1,4 @@
import { defineMessages, injectIntl } from 'react-intl';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
@ -6,8 +7,21 @@ import DeleteStackButton from './DeleteStackButton';
import InlineNotification from '../ui/InlineNotification';
import { deploymentStatusMessages } from '../../constants/StacksConstants';
export default class DeploymentFailure extends React.Component {
const messages = defineMessages({
deleteDeployment: {
id: 'DeploymentFailure.deleteDeployment',
defaultMessage: 'Delete Deployment'
},
requestingDeletion: {
id: 'DeploymentFailure.requestingDeletion',
defaultMessage: 'Requesting Deletion of Deployment'
}
});
class DeploymentFailure extends React.Component {
render() {
const { formatMessage } = this.props.intl;
return (
<div>
<InlineNotification type="error"
@ -17,11 +31,11 @@ export default class DeploymentFailure extends React.Component {
More details</Link>
</p>
</InlineNotification>
<DeleteStackButton content="Delete Deployment"
<DeleteStackButton content={formatMessage(messages.deleteDeployment)}
deleteStack={this.props.deleteStack}
disabled={this.props.isRequestingStackDelete}
loaded={!this.props.isRequestingStackDelete}
loaderContent="Requesting Deletion of Deployment"
loaderContent={formatMessage(messages.requestingDeletion)}
stack={this.props.stack}/>
</div>
);
@ -30,6 +44,9 @@ export default class DeploymentFailure extends React.Component {
DeploymentFailure.propTypes = {
deleteStack: React.PropTypes.func.isRequired,
intl: React.PropTypes.object,
isRequestingStackDelete: React.PropTypes.bool,
stack: ImmutablePropTypes.record.isRequired
};
export default injectIntl(DeploymentFailure);

View File

@ -1,4 +1,5 @@
import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
@ -9,22 +10,41 @@ import { getCurrentStack,
import { getAvailableNodes, getUnassignedAvailableNodes } from '../../selectors/nodes';
import { getEnvironmentConfigurationSummary } from '../../selectors/environmentConfiguration';
import { getCurrentPlan } from '../../selectors/plans';
import { ConfigurePlanStep } from './ConfigurePlanStep';
import ConfigurePlanStep from './ConfigurePlanStep';
import CurrentPlanActions from '../../actions/CurrentPlanActions';
import { DeploymentPlanStep } from './DeploymentPlanStep';
import { DeployStep } from './DeployStep';
import DeployStep from './DeployStep';
import EnvironmentConfigurationActions from '../../actions/EnvironmentConfigurationActions';
import { HardwareStep } from './HardwareStep';
import HardwareStep from './HardwareStep';
import PlansDropdown from './PlansDropdown';
import NodesActions from '../../actions/NodesActions';
import NoPlans from './NoPlans';
import NotificationActions from '../../actions/NotificationActions';
import PlanActions from '../../actions/PlansActions';
import StacksActions from '../../actions/StacksActions';
import { RolesStep } from './RolesStep';
import RolesStep from './RolesStep';
import RolesActions from '../../actions/RolesActions';
import ValidationsActions from '../../actions/ValidationsActions';
const messages = defineMessages({
hardwareStepHeader: {
id: 'DeploymentPlan.hardwareStepHeader',
defaultMessage: 'Prepare Hardware'
},
configureRolesStepHeader: {
id: 'DeploymentPlan.configureRolesStepHeader',
defaultMessage: 'Configure Roles and Assign Nodes'
},
deploymentConfigurationStepHeader: {
id: 'DeploymentPlan.deploymentConfigurationStepHeader',
defaultMessage: 'Specify Deployment Configuration'
},
deployStepHeader: {
id: 'DeploymentPlan.deployStepHeader',
defaultMessage: 'Deploy'
}
});
class DeploymentPlan extends React.Component {
componentDidMount() {
this.props.fetchStacks();
@ -52,6 +72,7 @@ class DeploymentPlan extends React.Component {
}
render() {
const { formatMessage } = this.props.intl;
let children;
const currentPlanName = this.props.hasPlans ? this.props.currentPlan.name : undefined;
@ -75,21 +96,21 @@ class DeploymentPlan extends React.Component {
</h1>
</div>
<ol className="deployment-step-list">
<DeploymentPlanStep title="Prepare Hardware"
disabled={this.props.currentStackDeploymentInProgress}>
<HardwareStep />
</DeploymentPlanStep>
<DeploymentPlanStep title="Specify Deployment Configuration"
disabled={this.props.currentStackDeploymentInProgress}>
<DeploymentPlanStep title={formatMessage(messages.hardwareStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}>
<HardwareStep />
</DeploymentPlanStep>
<DeploymentPlanStep title={formatMessage(messages.deploymentConfigurationStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}>
<ConfigurePlanStep
fetchEnvironmentConfiguration={this.props.fetchEnvironmentConfiguration}
summary={this.props.environmentConfigurationSummary}
planName={currentPlanName}
isFetching={this.props.isFetchingEnvironmentConfiguration}
loaded={this.props.environmentConfigurationLoaded}/>
</DeploymentPlanStep>
<DeploymentPlanStep title="Configure Roles and Assign Nodes"
disabled={this.props.currentStackDeploymentInProgress}>
</DeploymentPlanStep>
<DeploymentPlanStep title={formatMessage(messages.configureRolesStepHeader)}
disabled={this.props.currentStackDeploymentInProgress}>
<RolesStep availableNodes={this.props.availableNodes}
fetchNodes={this.props.fetchNodes}
fetchRoles={this.props.fetchRoles.bind(this, currentPlanName)}
@ -99,7 +120,7 @@ class DeploymentPlan extends React.Component {
rolesLoaded={this.props.rolesLoaded}
unassignedAvailableNodes={this.props.unassignedAvailableNodes}/>
</DeploymentPlanStep>
<DeploymentPlanStep title="Deploy">
<DeploymentPlanStep title={formatMessage(messages.deployStepHeader)}>
<DeployStep
currentPlan={this.props.currentPlan}
currentStack={this.props.currentStack}
@ -151,6 +172,7 @@ DeploymentPlan.propTypes = {
fetchStacks: React.PropTypes.func,
hasPlans: React.PropTypes.bool,
inactivePlans: ImmutablePropTypes.map,
intl: React.PropTypes.object,
isFetchingEnvironmentConfiguration: React.PropTypes.bool,
isFetchingNodes: React.PropTypes.bool,
isFetchingRoles: React.PropTypes.bool,
@ -213,4 +235,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DeploymentPlan);
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(DeploymentPlan));

View File

@ -1,6 +1,7 @@
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import { Link } from 'react-router';
import React from 'react';
import { deploymentStatusMessages as statusMessages,
stackStates } from '../../constants/StacksConstants';
@ -8,7 +9,26 @@ import DeleteStackButton from './DeleteStackButton';
import Loader from '../ui/Loader';
import ProgressBar from '../ui/ProgressBar';
export default class DeploymentProgress extends React.Component {
const messages = defineMessages({
cancelDeployment: {
id: 'DeploymentProgress.cancelDeployment',
defaultMessage: 'Cancel Deployment'
},
requestingDeletion: {
id: 'DeploymentProgress.requestingDeletion',
defaultMessage: 'Requesting Deletion of Deployment'
},
deploymentInProgress: {
id: 'DeploymentProgress.deploymentInProgress',
defaultMessage: 'Deployment is currently in progress.'
},
viewInformation: {
id: 'DeploymentProgress.viewInformation',
defaultMessage: 'View detailed information'
}
});
class DeploymentProgress extends React.Component {
renderProgressBar() {
return (
this.props.stack.stack_status === stackStates.CREATE_IN_PROGRESS ? (
@ -20,24 +40,27 @@ export default class DeploymentProgress extends React.Component {
}
render() {
const { formatMessage } = this.props.intl;
const statusMessage = (
<strong>{statusMessages[this.props.stack.stack_status]}</strong>
);
const deleteButton = this.props.stack.stack_status !== stackStates.DELETE_IN_PROGRESS
? (<DeleteStackButton content="Cancel Deployment"
? (<DeleteStackButton content={formatMessage(messages.cancelDeployment)}
buttonIconClass="fa fa-ban"
deleteStack={this.props.deleteStack}
disabled={this.props.isRequestingStackDelete}
loaded={!this.props.isRequestingStackDelete}
loaderContent="Requesting Deletion of Deployment"
loaderContent={formatMessage(messages.requestingDeletion)}
stack={this.props.stack}/>) : null;
return (
<div>
<p>
Deployment is currently in progress. <Link to="/deployment-plan/deployment-detail">
View detailed information
<span><FormattedMessage {...messages.deploymentInProgress}/> </span>
<Link to="/deployment-plan/deployment-detail">
<FormattedMessage {...messages.viewInformation}/>
</Link>
</p>
<div className="progress-description">
@ -53,6 +76,9 @@ export default class DeploymentProgress extends React.Component {
DeploymentProgress.propTypes = {
deleteStack: React.PropTypes.func.isRequired,
deploymentProgress: React.PropTypes.number.isRequired,
intl: React.PropTypes.object,
isRequestingStackDelete: React.PropTypes.bool.isRequired,
stack: ImmutablePropTypes.record.isRequired
};
export default injectIntl(DeploymentProgress);

View File

@ -1,12 +1,24 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import DeleteStackButton from './DeleteStackButton';
import { deploymentStatusMessages } from '../../constants/StacksConstants';
import InlineNotification from '../ui/InlineNotification';
import OvercloudInfo from '../deployment/OvercloudInfo';
export default class DeploymentSuccess extends React.Component {
const messages = defineMessages({
deleteDeployment: {
id: 'DeploymentSuccess.deleteDeployment',
defaultMessage: 'Delete Deployment'
},
requestingDeletion: {
id: 'DeploymentSuccess.requestingDeletion',
defaultMessage: 'Requesting Deletion of Deployment'
}
});
class DeploymentSuccess extends React.Component {
componentDidMount() {
this.props.fetchStackResource(this.props.stack, 'PublicVirtualIP');
this.props.fetchStackEnvironment(this.props.stack);
@ -14,6 +26,8 @@ export default class DeploymentSuccess extends React.Component {
}
render() {
const { formatMessage } = this.props.intl;
return (
<div>
<InlineNotification type="success"
@ -23,11 +37,11 @@ export default class DeploymentSuccess extends React.Component {
<OvercloudInfo stackResourcesLoaded={this.props.stackResourcesLoaded}
stack={this.props.stack}
stackResources={this.props.stackResources}/>
<DeleteStackButton content="Delete Deployment"
<DeleteStackButton content={formatMessage(messages.deleteDeployment)}
deleteStack={this.props.deleteStack}
disabled={this.props.isRequestingStackDelete}
loaded={!this.props.isRequestingStackDelete}
loaderContent="Requesting Deletion of Deployment"
loaderContent={formatMessage(messages.requestingDeletion)}
stack={this.props.stack}/>
</div>
);
@ -38,9 +52,12 @@ DeploymentSuccess.propTypes = {
deleteStack: React.PropTypes.func.isRequired,
fetchStackEnvironment: React.PropTypes.func.isRequired,
fetchStackResource: React.PropTypes.func.isRequired,
intl: React.PropTypes.object,
isRequestingStackDelete: React.PropTypes.bool,
runPostDeploymentValidations: React.PropTypes.func.isRequired,
stack: ImmutablePropTypes.record.isRequired,
stackResources: ImmutablePropTypes.map.isRequired,
stackResourcesLoaded: React.PropTypes.bool.isRequired
};
export default injectIntl(DeploymentSuccess);

View File

@ -1,10 +1,20 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import React from 'react';
export const HardwareStep = () => {
const messages = defineMessages({
registerNodes: {
id: 'HardwareStep.registerNodes',
defaultMessage: 'Register Nodes'
}
});
const HardwareStep = () => {
return (
<Link className="btn btn-default" to="/nodes/registered/register">
<span className="fa fa-plus"/> Register Nodes
<span className="fa fa-plus"/> <FormattedMessage {...messages.registerNodes}/>
</Link>
);
};
export default HardwareStep;

View File

@ -1,6 +1,22 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import React from 'react';
const messages = defineMessages({
noPlansAvailable: {
id: 'NoPlans.noPlansAvailable',
defaultMessage: 'No Deployment Plans Available'
},
noPlansAvailableMessage: {
id: 'NoPlans.noPlansAvailableMessage',
defaultMessage: 'There are no Deployment Plans available. Please create one first.'
},
createNewPlan: {
id: 'NoPlans.createNewPlan',
defaultMessage: 'Create New Plan'
}
});
export default class NoPlans extends React.Component {
render() {
return (
@ -8,13 +24,13 @@ export default class NoPlans extends React.Component {
<div className="blank-slate-pf-icon">
<span className="fa fa-ban"></span>
</div>
<h1>No Deployment Plans Available</h1>
<p>There are no Deployment Plans available. Please create one first.</p>
<h1><FormattedMessage {...messages.noPlansAvailable}/></h1>
<p><FormattedMessage {...messages.noPlansAvailableMessage}/></p>
<div className="blank-slate-pf-main-action">
<Link to="/plans/new"
query={{tab: 'newPlan'}}
className="btn btn-lg btn-primary">
<span className="fa fa-plus"/> Create New Plan
<span className="fa fa-plus"/> <FormattedMessage {...messages.createNewPlan}/>
</Link>
</div>
</div>

View File

@ -1,5 +1,6 @@
import * as _ from 'lodash';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage } from 'react-intl';
import Formsy from 'formsy-react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
@ -16,6 +17,17 @@ import Modal from '../ui/Modal';
import NodesActions from '../../actions/NodesActions';
import NodesTable from '../nodes/NodesTable';
const messages = defineMessages({
assignUnassignNodes: {
id: 'NodesAssignment.assignUnassignNodes',
defaultMessage: 'Assign/Unassign Selected Nodes'
},
done: {
id: 'NodesAssignment.done',
defaultMessage: 'Done'
}
});
class NodesAssignment extends React.Component {
componentDidMount() {
@ -36,7 +48,7 @@ class NodesAssignment extends React.Component {
<button className="btn btn-primary"
type="submit"
disabled={this.props.nodesOperationInProgress}>
Assign/Unassign Selected Nodes
<FormattedMessage {...messages.assignUnassignNodes}/>
</button>
</div>
);
@ -98,7 +110,7 @@ class NodesAssignment extends React.Component {
<div className="modal-footer">
<Link to="/deployment-plan" type="button" className="btn btn-default" >
Done
<FormattedMessage {...messages.done}/>
</Link>
</div>
</Formsy.Form>

View File

@ -1,11 +1,23 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import { Link } from 'react-router';
import React from 'react';
import DropdownItem from '../ui/dropdown/DropdownItem';
import DropdownButton from '../ui/dropdown/DropdownButton';
import Dropdown from '../ui/dropdown/Dropdown';
const messages = defineMessages({
manageDeployments: {
id: 'PlansDropdown.manageDeployments',
defaultMessage: 'Manage Deployments'
},
selectDeployment: {
id: 'PlansDropdown.selectDeployment',
defaultMessage: 'Select Deployment'
}
});
export default class PlansDropdown extends React.Component {
renderRecentPlans() {
return this.props.plans.toList().map(plan => {
@ -21,15 +33,21 @@ export default class PlansDropdown extends React.Component {
render() {
if(this.props.plans.isEmpty()) {
return (
<Link className="btn btn-link" to="/plans/list">Manage Deployments</Link>
<Link className="btn btn-link" to="/plans/list">
<FormattedMessage {...messages.manageDeployments}/>
</Link>
);
} else {
return (
<Dropdown>
<DropdownButton className="btn-link">Select Deployment</DropdownButton>
<DropdownButton className="btn-link">
<FormattedMessage {...messages.selectDeployment}/>
</DropdownButton>
{this.renderRecentPlans()}
<DropdownItem key="divider" divider/>
<DropdownItem key="plansLink" to="/plans/list">Manage Deployments</DropdownItem>
<DropdownItem key="plansLink" to="/plans/list">
<FormattedMessage {...messages.manageDeployments}/>
</DropdownItem>
</Dropdown>
);
}

View File

@ -1,6 +1,19 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import React from 'react';
import Link from '../ui/Link';
const messages = defineMessages({
nodesAssigned: {
id: 'RoleCard.nodesAssigned',
defaultMessage: 'Nodes assigned'
},
assignNodes: {
id: 'RoleCard.assignNodes',
defaultMessage: 'Assign Nodes'
}
});
export default class RoleCard extends React.Component {
render() {
const disabled = !this.props.availableNodesCount && !this.props.assignedNodesCount;
@ -22,7 +35,9 @@ export default class RoleCard extends React.Component {
</span>
<span className="card-pf-utilization-card-details-description">
<span className="card-pf-utilization-card-details-line-1">&nbsp;</span>
<span className="card-pf-utilization-card-details-line-2">Nodes assigned</span>
<span className="card-pf-utilization-card-details-line-2">
<FormattedMessage {...messages.nodesAssigned}/>
</span>
</span>
</p>
</div>
@ -32,7 +47,8 @@ export default class RoleCard extends React.Component {
disabled={disabled}
to={`/deployment-plan/${this.props.identifier}/assign-nodes`}
className="card-pf-link-with-icon">
<span className="pficon pficon-add-circle-o" />Assign Nodes
<span className="pficon pficon-add-circle-o" />
<FormattedMessage {...messages.assignNodes}/>
</Link>
</p>
</div>

View File

@ -1,11 +1,19 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import { getAssignedNodes } from '../../selectors/nodes';
import Loader from '../ui/Loader';
import RoleCard from './RoleCard';
export default class Roles extends React.Component {
const messages = defineMessages({
loadingDeploymentRoles: {
id: 'Roles.loadingDeploymentRoles',
defaultMessage: 'Loading Deployment Roles...'
}
});
class Roles extends React.Component {
componentDidMount() {
this.props.fetchRoles();
this.props.fetchNodes();
@ -45,7 +53,7 @@ export default class Roles extends React.Component {
<div className="panel panel-default roles-panel">
<div className="panel-body">
<Loader loaded={this.props.loaded}
content="Loading Deployment Roles..."
content={this.props.intl.formatMessage(messages.loadingDeploymentRoles)}
height={40}>
<div className="row-cards-pf">
{this.renderRoleCards()}
@ -60,9 +68,12 @@ Roles.propTypes = {
availableNodes: ImmutablePropTypes.map,
fetchNodes: React.PropTypes.func.isRequired,
fetchRoles: React.PropTypes.func.isRequired,
intl: React.PropTypes.object,
isFetchingNodes: React.PropTypes.bool,
isFetchingRoles: React.PropTypes.bool,
loaded: React.PropTypes.bool.isRequired,
roles: React.PropTypes.array.isRequired,
unassignedAvailableNodes: ImmutablePropTypes.map
};
export default injectIntl(Roles);

View File

@ -1,22 +1,31 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React from 'react';
import Loader from '../ui/Loader';
import Roles from './Roles';
export const RolesStep = ({ isFetchingNodes,
const messages = defineMessages({
loadingNodes: {
id: 'RolesStep.loadingNodes',
defaultMessage: 'Loading Nodes...'
}
});
const RolesStep = ({ isFetchingNodes,
availableNodes,
unassignedAvailableNodes,
roles,
fetchRoles,
fetchNodes,
intl,
isFetchingRoles,
rolesLoaded }) => {
return (
<div>
<p>
<Loader loaded={!isFetchingNodes}
content="Loading Nodes..."
content={intl.formatMessage(messages.loadingNodes)}
component="span"
inline>
<strong>{unassignedAvailableNodes.size}</strong> Nodes
@ -38,9 +47,12 @@ RolesStep.propTypes = {
availableNodes: ImmutablePropTypes.map.isRequired,
fetchNodes: React.PropTypes.func.isRequired,
fetchRoles: React.PropTypes.func.isRequired,
intl: React.PropTypes.object,
isFetchingNodes: React.PropTypes.bool.isRequired,
isFetchingRoles: React.PropTypes.bool.isRequired,
roles: ImmutablePropTypes.map.isRequired,
rolesLoaded: React.PropTypes.bool.isRequired,
unassignedAvailableNodes: ImmutablePropTypes.map.isRequired
};
export default injectIntl(RolesStep);

View File

@ -0,0 +1,46 @@
import { addLocaleData, IntlProvider } from 'react-intl';
import React from 'react';
import ja from 'react-intl/locale-data/ja';
import jaMessages from '../../../../i18n/locales/ja.json';
const MESSAGES = {
ja: jaMessages.messages
};
class I18nProvider extends React.Component {
constructor() {
super();
addLocaleData([...ja]);
this.state = {
locale: 'en'
};
}
componentWillMount() {
const locale = localStorage.getItem('language') ||
(navigator.languages && navigator.languages[0]) ||
navigator.language || navigator.userLanguage;
// We only use the country part of the locale:
const language = locale.substr(0, 2);
if(MESSAGES[language]) {
this.setState({ locale: language });
}
}
render() {
return (
<IntlProvider locale={this.state.locale} messages={MESSAGES[this.state.locale]}>
{this.props.children}
</IntlProvider>
);
}
}
I18nProvider.propTypes = {
children: React.PropTypes.node
};
export default I18nProvider;

View File

@ -3,7 +3,6 @@ import 'babel-polyfill';
import { Provider } from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import { IntlProvider } from 'react-intl';
import { Router, Route, Redirect } from 'react-router';
import { browserHistory } from 'react-router';
@ -18,6 +17,7 @@ import EditPlan from './components/plan/EditPlan';
import EnvironmentConfiguration from
'./components/environment_configuration/EnvironmentConfiguration.js';
import { getCurrentStackDeploymentInProgress } from './selectors/stacks';
import I18nProvider from './components/i18n/I18nProvider';
import initFormsy from './components/utils/Formsy';
import ListPlans from './components/plan/ListPlans';
import Login from './components/Login';
@ -41,6 +41,7 @@ import store from './store';
import '../less/base.less';
TempStorage.initialized.then(() => {
/**
* @function checkAuth
@ -134,9 +135,9 @@ TempStorage.initialized.then(() => {
ReactDOM.render(
<Provider store={store}>
<IntlProvider locale="en">
<I18nProvider>
<Router history={browserHistory}>{routes}</Router>
</IntlProvider>
</I18nProvider>
</Provider>,
document.getElementById('react-app-index')
);

View File

@ -85,6 +85,12 @@ module.exports = {
{
loader: __dirname + '/src/js/loaders/version.js',
test: /src\/js\/index.js$/
},
// JSON
{
test: /\.json$/,
loader: 'json'
}
]
},