From 57c98c657d63b882345769accfc7bd523b7a489f Mon Sep 17 00:00:00 2001 From: Florian Fuchs Date: Fri, 18 Nov 2016 15:44:26 +0100 Subject: [PATCH] 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 --- .babelrc | 8 ++- .gitignore | 2 + README.md | 44 ++++++++++++++++ i18n/locales/ja.json | 1 + package.json | 4 +- .../deployment_plan/ConfigurePlanStep.js | 16 ++++-- .../deployment_plan/DeleteStackButton.js | 23 ++++++-- .../components/deployment_plan/DeployStep.js | 25 ++++++--- .../DeploymentConfiguration.js | 30 +++++++++-- .../DeploymentConfigurationSummary.js | 15 +++++- .../deployment_plan/DeploymentFailure.js | 23 ++++++-- .../deployment_plan/DeploymentPlan.js | 52 +++++++++++++------ .../deployment_plan/DeploymentProgress.js | 38 +++++++++++--- .../deployment_plan/DeploymentSuccess.js | 25 +++++++-- .../deployment_plan/HardwareStep.js | 16 ++++-- src/js/components/deployment_plan/NoPlans.js | 22 ++++++-- .../deployment_plan/NodesAssignment.js | 16 +++++- .../deployment_plan/PlansDropdown.js | 26 ++++++++-- src/js/components/deployment_plan/RoleCard.js | 20 ++++++- src/js/components/deployment_plan/Roles.js | 17 ++++-- .../components/deployment_plan/RolesStep.js | 18 +++++-- src/js/components/i18n/I18nProvider.js | 46 ++++++++++++++++ src/js/index.js | 7 +-- webpack.config.js | 6 +++ 24 files changed, 428 insertions(+), 72 deletions(-) create mode 100644 i18n/locales/ja.json create mode 100644 src/js/components/i18n/I18nProvider.js diff --git a/.babelrc b/.babelrc index 9b7d435a..33677322 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,9 @@ { - "presets": ["es2015", "stage-0", "react"] + "presets": ["es2015", "stage-0", "react"], + "plugins": [ + ["react-intl", { + "messagesDir": "./i18n/extracted-messages/", + "enforceDescriptions": false + }] + ] } diff --git a/.gitignore b/.gitignore index 729bec59..3224c928 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ tripleo-ui-*.tar.gz .DS_Store .idea app.conf +messages.pot +i18n/extracted-messages* diff --git a/README.md b/README.md index ec80da05..71d22638 100644 --- a/README.md +++ b/README.md @@ -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 +}; +``` diff --git a/i18n/locales/ja.json b/i18n/locales/ja.json new file mode 100644 index 00000000..21cd9ea6 --- /dev/null +++ b/i18n/locales/ja.json @@ -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":""}} diff --git a/package.json b/package.json index 182e9d2e..03fa1886 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/js/components/deployment_plan/ConfigurePlanStep.js b/src/js/components/deployment_plan/ConfigurePlanStep.js index 263e6512..09ccf91a 100644 --- a/src/js/components/deployment_plan/ConfigurePlanStep.js +++ b/src/js/components/deployment_plan/ConfigurePlanStep.js @@ -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 (
  - Edit Configuration +
); @@ -21,3 +29,5 @@ ConfigurePlanStep.propTypes = { planName: React.PropTypes.string.isRequired, summary: React.PropTypes.string.isRequired }; + +export default ConfigurePlanStep; diff --git a/src/js/components/deployment_plan/DeleteStackButton.js b/src/js/components/deployment_plan/DeleteStackButton.js index 3344eac7..79f0501f 100644 --- a/src/js/components/deployment_plan/DeleteStackButton.js +++ b/src/js/components/deployment_plan/DeleteStackButton.js @@ -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 (
{ +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 ( @@ -23,7 +31,8 @@ export const DeployStep = ({ currentPlan, currentStack, currentStackResources, content="Requesting a deploy..." component="span" inline> - Validate and Deploy + @@ -69,3 +78,5 @@ DeployStep.propTypes = { runPostDeploymentValidations: React.PropTypes.func.isRequired, stacksLoaded: React.PropTypes.bool.isRequired }; + +export default DeployStep; diff --git a/src/js/components/deployment_plan/DeploymentConfiguration.js b/src/js/components/deployment_plan/DeploymentConfiguration.js index 97b94d62..10810c40 100644 --- a/src/js/components/deployment_plan/DeploymentConfiguration.js +++ b/src/js/components/deployment_plan/DeploymentConfiguration.js @@ -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">
{React.cloneElement(this.props.children, diff --git a/src/js/components/deployment_plan/DeploymentConfigurationSummary.js b/src/js/components/deployment_plan/DeploymentConfigurationSummary.js index d8876d2b..e20bbe3b 100644 --- a/src/js/components/deployment_plan/DeploymentConfigurationSummary.js +++ b/src/js/components/deployment_plan/DeploymentConfigurationSummary.js @@ -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 ( {this.props.summary} @@ -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); diff --git a/src/js/components/deployment_plan/DeploymentFailure.js b/src/js/components/deployment_plan/DeploymentFailure.js index b0ff5eed..fb7d60ff 100644 --- a/src/js/components/deployment_plan/DeploymentFailure.js +++ b/src/js/components/deployment_plan/DeploymentFailure.js @@ -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 (

-
); @@ -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); diff --git a/src/js/components/deployment_plan/DeploymentPlan.js b/src/js/components/deployment_plan/DeploymentPlan.js index 7b2e6126..fb02d59b 100644 --- a/src/js/components/deployment_plan/DeploymentPlan.js +++ b/src/js/components/deployment_plan/DeploymentPlan.js @@ -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 {
    - - - - + + + + - - + + - + {statusMessages[this.props.stack.stack_status]} ); const deleteButton = this.props.stack.stack_status !== stackStates.DELETE_IN_PROGRESS - ? () : null; return (

    - Deployment is currently in progress. - View detailed information + + +

    @@ -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); diff --git a/src/js/components/deployment_plan/DeploymentSuccess.js b/src/js/components/deployment_plan/DeploymentSuccess.js index f91334ec..8e2e5e98 100644 --- a/src/js/components/deployment_plan/DeploymentSuccess.js +++ b/src/js/components/deployment_plan/DeploymentSuccess.js @@ -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 (
    -
    ); @@ -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); diff --git a/src/js/components/deployment_plan/HardwareStep.js b/src/js/components/deployment_plan/HardwareStep.js index 14de2fe0..366efc04 100644 --- a/src/js/components/deployment_plan/HardwareStep.js +++ b/src/js/components/deployment_plan/HardwareStep.js @@ -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 ( - Register Nodes + ); }; + +export default HardwareStep; diff --git a/src/js/components/deployment_plan/NoPlans.js b/src/js/components/deployment_plan/NoPlans.js index 60497a94..29de6f77 100644 --- a/src/js/components/deployment_plan/NoPlans.js +++ b/src/js/components/deployment_plan/NoPlans.js @@ -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 {
    -

    No Deployment Plans Available

    -

    There are no Deployment Plans available. Please create one first.

    +

    +

    - Create New Plan +
    diff --git a/src/js/components/deployment_plan/NodesAssignment.js b/src/js/components/deployment_plan/NodesAssignment.js index e981c355..b16c78f6 100644 --- a/src/js/components/deployment_plan/NodesAssignment.js +++ b/src/js/components/deployment_plan/NodesAssignment.js @@ -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 {
    ); @@ -98,7 +110,7 @@ class NodesAssignment extends React.Component {
    - Done +
    diff --git a/src/js/components/deployment_plan/PlansDropdown.js b/src/js/components/deployment_plan/PlansDropdown.js index c32e9daa..7110585b 100644 --- a/src/js/components/deployment_plan/PlansDropdown.js +++ b/src/js/components/deployment_plan/PlansDropdown.js @@ -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 ( - Manage Deployments + + + ); } else { return ( - Select Deployment + + + {this.renderRecentPlans()} - Manage Deployments + + + ); } diff --git a/src/js/components/deployment_plan/RoleCard.js b/src/js/components/deployment_plan/RoleCard.js index b80a1ac8..203d994f 100644 --- a/src/js/components/deployment_plan/RoleCard.js +++ b/src/js/components/deployment_plan/RoleCard.js @@ -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 {   - Nodes assigned + + +

    @@ -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"> - Assign Nodes + +

    diff --git a/src/js/components/deployment_plan/Roles.js b/src/js/components/deployment_plan/Roles.js index b7135a64..5e2f9ea2 100644 --- a/src/js/components/deployment_plan/Roles.js +++ b/src/js/components/deployment_plan/Roles.js @@ -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 {
    {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); diff --git a/src/js/components/deployment_plan/RolesStep.js b/src/js/components/deployment_plan/RolesStep.js index 1955a7b6..b8d51f55 100644 --- a/src/js/components/deployment_plan/RolesStep.js +++ b/src/js/components/deployment_plan/RolesStep.js @@ -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 (

    {unassignedAvailableNodes.size} 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); diff --git a/src/js/components/i18n/I18nProvider.js b/src/js/components/i18n/I18nProvider.js new file mode 100644 index 00000000..7d33415f --- /dev/null +++ b/src/js/components/i18n/I18nProvider.js @@ -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 ( + + {this.props.children} + + ); + } +} + +I18nProvider.propTypes = { + children: React.PropTypes.node +}; + +export default I18nProvider; diff --git a/src/js/index.js b/src/js/index.js index 7ce15b53..365d6798 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -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( - + {routes} - + , document.getElementById('react-app-index') ); diff --git a/webpack.config.js b/webpack.config.js index 3a39aeb0..4ccf8abf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -85,6 +85,12 @@ module.exports = { { loader: __dirname + '/src/js/loaders/version.js', test: /src\/js\/index.js$/ + }, + + // JSON + { + test: /\.json$/, + loader: 'json' } ] },