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:
Florian Fuchs 2016-12-16 15:17:47 +01:00
parent 532e60e208
commit 941b709064
11 changed files with 235 additions and 21 deletions

View File

@ -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']
};

View File

@ -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
};
}
};

View File

@ -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}/>

View File

@ -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);

View File

@ -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);

View File

@ -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
};

View File

@ -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'
};

View File

@ -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,

View File

@ -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;
}
}

13
src/js/selectors/i18n.js Normal file
View File

@ -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]
);

View File

@ -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;
}