Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-language capability to blip with react-i18next #468

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ecmaFeatures": {
"jsx": true,
"classes": true,
"modules": true,
"modules": true
},
"env": {
// I write for browser
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ artifact_node.sh

# GitBook
_book/

# Use yarn.lock
package-lock.json

# Ignore old translation files
locales/*/*_old.json
7 changes: 4 additions & 3 deletions app/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { render } from 'react-dom';
import bows from 'bows';
import _ from 'lodash';

import './core/language'; // Set the language before loading components
import blipCreateStore from './redux/store';
import AppRoot from './redux/containers/Root';

Expand All @@ -42,10 +43,10 @@ var appContext = {
config: config
};

// This anonymous function must remain in ES5 format because
// This anonymous function must remain in ES5 format because
// the argument parameter used is not bound when using arrow functions
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
appContext.trackMetric = function() {
appContext.trackMetric = function() {
var args = Array.prototype.slice.call(arguments);
return appContext.api.metrics.track.apply(appContext.api.metrics, args);
};
Expand Down Expand Up @@ -84,7 +85,7 @@ appContext.init = callback => {
* This renders the AppComponent into the DOM providing appContext
* as the context for AppComponent so that the required dependencies
* are passed in!
*
*
*/
appContext.start = () => {

Expand Down
9 changes: 5 additions & 4 deletions app/components/chart/trends.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import bows from 'bows';
import React, { PropTypes, PureComponent } from 'react';
import ReactDOM from 'react-dom';
import sundial from 'sundial';
import { translate } from 'react-i18next';

import Header from './header';
import SubNav from './trendssubnav';
Expand All @@ -35,7 +36,7 @@ const TrendsContainer = viz.containers.TrendsContainer;
const reshapeBgClassesToBgBounds = viz.utils.reshapeBgClassesToBgBounds;
const Loader = viz.components.Loader;

class Trends extends PureComponent {
const Trends = translate()(class extends PureComponent {
static propTypes = {
bgPrefs: PropTypes.object.isRequired,
chartPrefs: PropTypes.object.isRequired,
Expand Down Expand Up @@ -105,15 +106,15 @@ class Trends extends PureComponent {
}

formatDate(datetime) {
const timePrefs = this.props.timePrefs
const { timePrefs, t } = this.props;
let timezone;
if (!timePrefs.timezoneAware) {
timezone = 'UTC';
}
else {
timezone = timePrefs.timezoneName || 'UTC';
}
return sundial.formatInTimezone(datetime, timezone, 'MMM D, YYYY');
return sundial.formatInTimezone(datetime, timezone, t('MMM D, YYYY'));
}

getNewDomain(current, extent) {
Expand Down Expand Up @@ -500,6 +501,6 @@ class Trends extends PureComponent {
focusedPoint={this.props.trendsState[currentPatientInViewId].focusedSmbg} />
);
}
}
});

export default Trends;
8 changes: 5 additions & 3 deletions app/components/chart/weekly.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var bows = require('bows');
var React = require('react');
var ReactDOM = require('react-dom');
var sundial = require('sundial');
import { translate } from 'react-i18next';

// tideline dependencies & plugins
var tidelineBlip = require('tideline/plugins/blip');
Expand Down Expand Up @@ -150,7 +151,7 @@ var WeeklyChart = React.createClass({
}
});

var Weekly = React.createClass({
var Weekly = translate()(React.createClass({
chartType: 'weekly',
log: bows('Weekly View'),
propTypes: {
Expand Down Expand Up @@ -297,8 +298,9 @@ var Weekly = React.createClass({
},

formatDate: function(datetime) {
const { t } = this.props;
// even when timezoneAware, labels should be generated as if UTC; just trust me (JEB)
return sundial.formatInTimezone(datetime, 'UTC', 'MMM D, YYYY');
return sundial.formatInTimezone(datetime, 'UTC', t('MMM D, YYYY'));
},

getTitle: function(datetimeLocationEndpoints) {
Expand Down Expand Up @@ -411,6 +413,6 @@ var Weekly = React.createClass({
}
this.setState({showingValues: !this.state.showingValues});
}
});
}));

module.exports = Weekly;
14 changes: 8 additions & 6 deletions app/components/loginnav/loginnav.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
*/

var React = require('react');
import { translate } from 'react-i18next';
var Link = require('react-router').Link;

var LoginNav = React.createClass({
var LoginNav = translate()(React.createClass({
propTypes: {
page: React.PropTypes.string,
hideLinks: React.PropTypes.bool,
Expand Down Expand Up @@ -45,12 +46,13 @@ var LoginNav = React.createClass({
return null;
}


var self = this;
var page = this.props.page;
const {page, t} = this.props;
var href = '/signup';
var className = 'js-signup-link';
var icon = 'icon-add';
var text = 'Sign up';
var text = t('Sign up');
var handleClick = function() {
self.props.trackMetric('Clicked Sign Up Link');
};
Expand All @@ -59,7 +61,7 @@ var LoginNav = React.createClass({
href = '/login';
className = 'js-login-link';
icon = 'icon-login';
text = 'Log in';
text = t('Log in');
handleClick = function() {
self.props.trackMetric('Clicked Log In Link');
};
Expand All @@ -71,6 +73,6 @@ var LoginNav = React.createClass({
className={className}><i className={icon}></i>{' ' + text}</Link>
);
}
});
}));

module.exports = LoginNav;
module.exports = LoginNav;
19 changes: 9 additions & 10 deletions app/components/navbar/navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
var React = require('react');
var IndexLink = require('react-router').IndexLink;
var Link = require('react-router').Link;
import { translate } from 'react-i18next';

var _ = require('lodash');
var cx = require('classnames');
Expand All @@ -26,7 +27,7 @@ var NavbarPatientCard = require('../../components/navbarpatientcard');

var logoSrc = require('./images/tidepool-logo-408x46.png');

var Navbar = React.createClass({
export default translate('translation', {withRef: true})(React.createClass({
propTypes: {
currentPage: React.PropTypes.string,
user: React.PropTypes.object,
Expand Down Expand Up @@ -128,7 +129,7 @@ var Navbar = React.createClass({

renderMenuSection: function() {
var currentPage = (this.props.currentPage && this.props.currentPage[0] === '/') ? this.props.currentPage.slice(1) : this.props.currentPage;
var user = this.props.user;
const {user, t} = this.props;

if (_.isEmpty(user)) {
return <div className="Navbar-menuSection"></div>;
Expand Down Expand Up @@ -180,7 +181,7 @@ var Navbar = React.createClass({
<div onClick={this.toggleDropdown}>
<i className='Navbar-icon Navbar-icon-profile icon-profile'></i>
<div className="Navbar-logged">
<span className="Navbar-loggedInAs">{'Logged in as '}</span>
<span className="Navbar-loggedInAs">{t('Logged in as ')}</span>
<span className="Navbar-userName" ref="userFullName" title={displayName}>{displayName}</span>
</div>
<i className='Navbar-icon Navbar-icon-down icon-arrow-down'></i>
Expand All @@ -189,13 +190,13 @@ var Navbar = React.createClass({
<div onClick={this.stopPropagation} className={dropdownClasses}>
<ul>
<li>
<Link to="/profile" title="Account" onClick={handleClickUser} className={accountSettingsClasses}>
<i className='Navbar-icon icon-settings'></i><span className="Navbar-menuText">Account Settings</span>
<Link to="/profile" title={t('Account')} onClick={handleClickUser} className={accountSettingsClasses}>
<i className='Navbar-icon icon-settings'></i><span className="Navbar-menuText">{t('Account Settings')}</span>
</Link>
</li>
<li>
<a href="" title="Logout" onClick={this.handleLogout} className="Navbar-button" ref="logout">
<i className='Navbar-icon icon-logout'></i><span className="Navbar-menuText">Logout</span>
<a href="" title={t('Logout')} onClick={this.handleLogout} className="Navbar-button" ref="logout">
<i className='Navbar-icon icon-logout'></i><span className="Navbar-menuText">{t('Logout')}</span>
</a>
</li>
</ul>
Expand Down Expand Up @@ -225,6 +226,4 @@ var Navbar = React.createClass({
logout();
}
}
});

module.exports = Navbar;
}));
49 changes: 49 additions & 0 deletions app/core/language.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

import i18n from 'i18next';
import { reactI18nextModule } from 'react-i18next';
import getLocale from 'browser-locale';
import moment from 'moment';

// Update moment with the right language, for date display
i18n.on('languageChanged', lng => moment.locale(lng));

i18n
.use(reactI18nextModule)
.init({
fallbackLng: 'en',
// i18next-browser-languagedetector doesn't work in my experience
lng: getLocale(),

// To allow . in keys
keySeparator: false,
// To allow : in keys
nsSeparator: '|',

debug: true,

interpolation: {
escapeValue: false, // not needed for react!!
},

// If the translation is empty, return the key instead
returnEmptyString: false,

react: {
wait: true,
// Needed for react < 16
defaultTransParent: 'div'
},

resources: {
en: {
// Default namespace
translation: require('../../locales/en/translation.json')
},
fr: {
// Default namespace
translation: require('../../locales/fr/translation.json')
}
}
});

export default i18n;
7 changes: 6 additions & 1 deletion app/pages/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import React from 'react';
import async from 'async';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import i18next from 'i18next';

import * as actions from '../../redux/actions';

Expand Down Expand Up @@ -99,7 +100,7 @@ export class AppComponent extends React.Component {
var navbar = this.refs.navbar;

if (navbar) {
navbar.hideDropdown();
navbar.getWrappedInstance().hideDropdown();
}
}

Expand Down Expand Up @@ -418,6 +419,10 @@ export function mapStateToProps(state) {
if (state.blip.loggedInUserId === state.blip.currentPatientInViewId) {
userIsCurrentPatient = true;
}

if (user && user.profile && user.profile.language) {
i18next.changeLanguage(user.profile.language);
}
}

if (state.blip.currentPatientInViewId) {
Expand Down
20 changes: 13 additions & 7 deletions app/pages/login/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';

import * as actions from '../../redux/actions';

Expand All @@ -30,7 +31,7 @@ import LoginNav from '../../components/loginnav';
import LoginLogo from '../../components/loginlogo';
import SimpleForm from '../../components/simpleform';

export let Login = React.createClass({
export let Login = translate()(React.createClass({
propTypes: {
acknowledgeNotification: React.PropTypes.func.isRequired,
confirmSignup: React.PropTypes.func.isRequired,
Expand All @@ -44,10 +45,12 @@ export let Login = React.createClass({
},

formInputs: function() {
const { t } = this.props;

return [
{ name: 'username', placeholder: 'Email', type: 'email', disabled: !!this.props.seedEmail },
{ name: 'password', placeholder: 'Password', type: 'password' },
{ name: 'remember', label: 'Remember me', type: 'checkbox' }
{ name: 'username', placeholder: t('Email'), type: 'email', disabled: !!this.props.seedEmail },
{ name: 'password', placeholder: t('Password'), type: 'password' },
{ name: 'remember', label: t('Remember me'), type: 'checkbox' }
];
},

Expand Down Expand Up @@ -100,7 +103,9 @@ export let Login = React.createClass({
},

renderForm: function() {
var submitButtonText = this.props.working ? 'Logging in...' : 'Login';
const { t } = this.props;

var submitButtonText = this.props.working ? t('Logging in...') : t('Login');
var forgotPassword = this.renderForgotPassword();

return (
Expand All @@ -122,7 +127,8 @@ export let Login = React.createClass({
},

renderForgotPassword: function() {
return <Link to="/request-password-reset">Forgot your password?</Link>;
const { t } = this.props;
return <Link to="/request-password-reset">{t('Forgot your password?')}</Link>;
},

handleSubmit: function(formValues) {
Expand Down Expand Up @@ -202,7 +208,7 @@ export let Login = React.createClass({
componentWillMount: function() {
this.doFetching(this.props);
}
});
}));

/**
* Expose "Smart" Component that is connect-ed to Redux
Expand Down
Loading