Adds language selector to NavBar
This patch adds: 1. Action, reducer and component changes to switch the language. 2. A DropdownToggle component that is generic than the existing Dropdown component, so it can be used inside the NavBar component. 3. CSS changes for the NavBar dropdown. Implements: blueprint tripleo-ui-i18n-support-for-js Change-Id: I83947ef8716c0be522b4eae763ef12b1343dc0a3
This commit is contained in:
parent
532e60e208
commit
941b709064
|
@ -15,6 +15,10 @@ window.tripleOUiConfig = {
|
|||
// Default websocket queue name
|
||||
// "zaqar_default_queue": "tripleo",
|
||||
|
||||
// Languages
|
||||
// If you choose more than one language, a language switcher will appear in the navigation bar.
|
||||
// "languages": ["en"],
|
||||
|
||||
// Logging
|
||||
// "loggers": ['console']
|
||||
};
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { getAppConfig } from '../services/utils';
|
||||
|
||||
export default {
|
||||
detectLanguage(messages) {
|
||||
const configLanguages = getAppConfig().languages;
|
||||
let language;
|
||||
// If the configuration contains only one language and there
|
||||
// are messages for it, return it;
|
||||
if(configLanguages && configLanguages.length === 1 && messages[configLanguages[0]]) {
|
||||
language = configLanguages[0];
|
||||
}
|
||||
else {
|
||||
const locale = localStorage.getItem('language') ||
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language || navigator.userLanguage;
|
||||
// If the locale contains the country but we can't find
|
||||
// messages for it then we only use the country part:
|
||||
language = (locale.match(/^[A-Za-z]+-[A-Za-z]+$/) && !messages[locale])
|
||||
? locale.split('-')[0]
|
||||
: locale;
|
||||
}
|
||||
return {
|
||||
type: 'DETECT_LANGUAGE',
|
||||
payload: {
|
||||
language,
|
||||
messages
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
chooseLanguage(language) {
|
||||
localStorage.setItem('language', language);
|
||||
return {
|
||||
type: 'CHOOSE_LANGUAGE',
|
||||
payload: language
|
||||
};
|
||||
}
|
||||
};
|
|
@ -3,7 +3,9 @@ import React from 'react';
|
|||
import { Link } from 'react-router';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { getAppConfig } from '../services/utils';
|
||||
import NavTab from './ui/NavTab';
|
||||
import I18nDropdown from './i18n/I18nDropdown';
|
||||
|
||||
import TripleoOwlSvg from '../../img/tripleo-owl.svg';
|
||||
|
||||
|
@ -33,6 +35,18 @@ export default class NavBar extends React.Component {
|
|||
this.props.onLogout();
|
||||
}
|
||||
|
||||
_renderLanguageDropdown() {
|
||||
const languages = getAppConfig().languages || [];
|
||||
|
||||
// Only include the I18nDropdown if there's more than one
|
||||
// language to choose from.
|
||||
return (languages.length > 1) ? (
|
||||
<li>
|
||||
<I18nDropdown/>
|
||||
</li>
|
||||
) : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav className="navbar navbar-default navbar-pf navbar-fixed-top" role="navigation">
|
||||
|
@ -59,6 +73,7 @@ export default class NavBar extends React.Component {
|
|||
{this.props.user.get('username')}
|
||||
</a>
|
||||
</li>
|
||||
{this._renderLanguageDropdown()}
|
||||
<li>
|
||||
<a href="#" onClick={this.logout.bind(this)} id="NavBar__logoutLink">
|
||||
<FormattedMessage {...messages.logoutLink}/>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
import Dropdown from '../ui/dropdown/Dropdown';
|
||||
import DropdownToggle from '../ui/dropdown/DropdownToggle';
|
||||
import DropdownItem from '../ui/dropdown/DropdownItem';
|
||||
import { getAppConfig } from '../../services/utils';
|
||||
import I18nActions from '../../actions/I18nActions';
|
||||
import { MESSAGES } from './I18nProvider';
|
||||
|
||||
const messages = defineMessages({
|
||||
language: {
|
||||
id: 'I18nDropdown.language',
|
||||
defaultMessage: 'Language'
|
||||
}
|
||||
});
|
||||
|
||||
const languages = {
|
||||
en: 'English',
|
||||
ja: 'Japanese'
|
||||
};
|
||||
|
||||
class I18nDropdown extends React.Component {
|
||||
_renderDropdownItems() {
|
||||
const configLanguages = getAppConfig().languages || [];
|
||||
return configLanguages.map((lang) => {
|
||||
return (MESSAGES[lang] || lang === 'en') ? (
|
||||
<DropdownItem key={`lang-${lang}`} onClick={this.props.chooseLanguage.bind(this, lang)}>
|
||||
{languages[lang]}
|
||||
</DropdownItem>
|
||||
) : null;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownToggle>
|
||||
<FormattedMessage {...messages.language}/> <b className="caret"></b>
|
||||
</DropdownToggle>
|
||||
{this._renderDropdownItems()}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
I18nDropdown.propTypes = {
|
||||
chooseLanguage: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
chooseLanguage: (language) => dispatch(I18nActions.chooseLanguage(language))
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(I18nDropdown);
|
|
@ -1,38 +1,29 @@
|
|||
import { addLocaleData, IntlProvider } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
|
||||
import I18nActions from '../../actions/I18nActions';
|
||||
import jaMessages from '../../../../i18n/locales/ja.json';
|
||||
import { getLanguage, getMessages } from '../../selectors/i18n';
|
||||
|
||||
|
||||
const MESSAGES = {
|
||||
export const MESSAGES = {
|
||||
ja: jaMessages.ja
|
||||
};
|
||||
|
||||
|
||||
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 });
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.detectLanguage(MESSAGES);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IntlProvider locale={this.state.locale} messages={MESSAGES[this.state.locale]}>
|
||||
<IntlProvider locale={this.props.language} messages={this.props.messages}>
|
||||
{this.props.children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
@ -40,7 +31,27 @@ class I18nProvider extends React.Component {
|
|||
}
|
||||
|
||||
I18nProvider.propTypes = {
|
||||
children: React.PropTypes.node
|
||||
children: React.PropTypes.node,
|
||||
detectLanguage: React.PropTypes.func.isRequired,
|
||||
language: React.PropTypes.string,
|
||||
messages: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default I18nProvider;
|
||||
I18nProvider.defaultProps = {
|
||||
messages: {}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
detectLanguage: (language) => dispatch(I18nActions.detectLanguage(language))
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
language: getLanguage(state),
|
||||
messages: getMessages(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(I18nProvider);
|
||||
|
|
|
@ -17,7 +17,8 @@ export default class Dropdown extends React.Component {
|
|||
render() {
|
||||
const children = React.Children.toArray(this.props.children);
|
||||
|
||||
const toggle = _.first(children, child => child.type.name === 'DropdownButton');
|
||||
const toggle = _.first(children, child => _.includes(['DropdownButton', 'DropdownToggle'],
|
||||
child.type.name));
|
||||
|
||||
const items = _.map(_.filter(children, child => child.type.name === 'DropdownItem'),
|
||||
item => React.cloneElement(
|
||||
|
@ -25,8 +26,8 @@ export default class Dropdown extends React.Component {
|
|||
|
||||
// Any other children are prepended to DropdownButton.
|
||||
// This can be used to add buttons to Dropdown button group
|
||||
const otherChildren = _.reject(children, child => _.includes(['DropdownButton', 'DropdownItem'],
|
||||
child.type.name));
|
||||
const otherChildren = _.reject(children, child => _.includes(
|
||||
['DropdownButton', 'DropdownToggle', 'DropdownItem'], child.type.name));
|
||||
const dropdownClasses = {
|
||||
open: this.state.isOpen
|
||||
};
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
export default class DropdownToggle extends React.Component {
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.toggleDropdown();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a className={this.props.className}
|
||||
data-toggle="dropdown"
|
||||
onClick={this.handleClick.bind(this)}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
DropdownToggle.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
className: React.PropTypes.string,
|
||||
toggleDropdown: React.PropTypes.func
|
||||
};
|
||||
|
||||
DropdownToggle.defaultProps = {
|
||||
className: 'dropdown-toggle'
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import currentPlanReducer from './currentPlanReducer';
|
||||
import environmentConfigurationReducer from './environmentConfigurationReducer';
|
||||
import i18nReducer from './i18nReducer';
|
||||
import loginReducer from './loginReducer';
|
||||
import nodesReducer from './nodesReducer';
|
||||
import notificationsReducer from './notificationsReducer';
|
||||
|
@ -16,6 +17,7 @@ const appReducer = combineReducers({
|
|||
currentPlan: currentPlanReducer,
|
||||
environmentConfiguration: environmentConfigurationReducer,
|
||||
executions: workflowExecutionsReducer,
|
||||
i18n: i18nReducer,
|
||||
login: loginReducer,
|
||||
nodes: nodesReducer,
|
||||
notifications: notificationsReducer,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Map } from 'immutable';
|
||||
|
||||
const initialState = Map({
|
||||
language: 'en',
|
||||
messages: {}
|
||||
});
|
||||
|
||||
export default function plansReducer(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
|
||||
case 'DETECT_LANGUAGE':
|
||||
return state
|
||||
.set('language', action.payload.language)
|
||||
.set('messages', action.payload.messages);
|
||||
|
||||
case 'CHOOSE_LANGUAGE':
|
||||
return state.set('language', action.payload);
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
const languageSelector = state => state.i18n.get('language');
|
||||
|
||||
const messagesSelector = state => state.i18n.get('messages');
|
||||
|
||||
export const getLanguage = createSelector(
|
||||
[languageSelector], language => language
|
||||
);
|
||||
|
||||
export const getMessages = createSelector(
|
||||
[languageSelector, messagesSelector], (language, messages) => messages[language]
|
||||
);
|
|
@ -53,3 +53,25 @@
|
|||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// Nav dropdown menu icons
|
||||
.navbar-pf .navbar-utility .dropdown a.dropdown-toggle {
|
||||
display: block;
|
||||
padding: 11px 10px 9px 10px;
|
||||
color: @navbar-pf-color;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #2b2b2b;
|
||||
}
|
||||
.navbar-pf .navbar-utility .dropdown.open a.dropdown-toggle,
|
||||
.navbar-pf .navbar-utility .dropdown a.dropdown-toggle:hover,
|
||||
.navbar-pf .navbar-utility .dropdown a.dropdown-toggle:active {
|
||||
background-color: #232323;
|
||||
color: @navbar-pf-active-color;
|
||||
}
|
||||
.navbar-pf .navbar-utility div.dropdown > .dropdown-toggle .pficon,
|
||||
.navbar-pf .navbar-utility li.dropdown > .dropdown-toggle .pficon {
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue