Skip to content

Commit

Permalink
Add react-intl (#166 & #163)
Browse files Browse the repository at this point in the history
* adding it

* added intl polyfill for safari

* had forgotten react-intl in package.json

* added app-specific localization data

* remove intl page and add intl in demo pages

* run ava with harmony-proxies enabled in node
  • Loading branch information
somus committed Jun 16, 2016
1 parent 494a713 commit 7ac297b
Show file tree
Hide file tree
Showing 26 changed files with 351 additions and 69 deletions.
11 changes: 0 additions & 11 deletions .babelrc
Expand Up @@ -4,17 +4,6 @@
"env": {
"production": {
"presets": ["es2015", "react", "react-optimize", "es2015-native-modules", "stage-0"]
},
"test": {
"plugins": [
[
"babel-plugin-webpack-loaders",
{
"config": "${CONFIG}",
"verbose": false
}
]
]
}
}
}
31 changes: 31 additions & 0 deletions Intl/localizationData/en.js
@@ -0,0 +1,31 @@
export default {
locale: 'en',
messages: {
siteTitle: 'MERN Starter Blog',
addPost: 'Add Post',
switchLanguage: 'Switch Language',
twitterMessage: 'We are on Twitter',
by: 'By',
deletePost: 'Delete Post',
createNewPost: 'Create new post',
authorName: 'Author\'s Name',
postTitle: 'Post Title',
postContent: 'Post Content',
submit: 'Submit',
comment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
other {has # comments}
}`,
HTMLComment: `user <b style='font-weight: bold'>{name} </b> {value, plural,
=0 {does not have <i style='font-style: italic'>any</i> comments}
=1 {has <i style='font-style: italic'>#</i> comment}
other {has <i style='font-style: italic'>#</i> comments}
}`,
nestedDateComment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
other {has # comments}
} as of {date}`,
},
};
31 changes: 31 additions & 0 deletions Intl/localizationData/fr.js
@@ -0,0 +1,31 @@
export default {
locale: 'fr',
messages: {
siteTitle: 'MERN blog de démarrage',
addPost: 'Ajouter Poster',
switchLanguage: 'Changer de langue',
twitterMessage: 'Nous sommes sur Twitter',
by: 'Par',
deletePost: 'Supprimer le message',
createNewPost: 'Créer un nouveau message',
authorName: 'Nom de l\'auteur',
postTitle: 'Titre de l\'article',
postContent: 'Contenu après',
submit: 'Soumettre',
comment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
other {has # comments}
} (in real app this would be translated to French)`,
HTMLComment: `user <b style='font-weight: bold'>{name} </b> {value, plural,
=0 {does not have <i style='font-style: italic'>any</i> comments}
=1 {has <i style='font-style: italic'>#</i> comment}
other {has <i style='font-style: italic'>#</i> comments}
} (in real app this would be translated to French)`,
nestedDateComment: `user {name} {value, plural,
=0 {does not have any comments}
=1 {has # comment}
other {has # comments}
} as of {date} (in real app this would be translated to French)`,
},
};
51 changes: 51 additions & 0 deletions Intl/setup.js
@@ -0,0 +1,51 @@
// list of available languages
export const enabledLanguages = [
'en',
'fr',
];

// this object will have language-specific data added to it which will be placed in the state when that language is active
// if localization data get to big, stop importing in all languages and switch to using API requests to load upon switching languages
export const localizationData = {};

// here you bring in 'intl' browser polyfill and language-specific polyfills
// (needed as safari doesn't have native intl: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)
// as well as react-intl's language-specific data
// be sure to use static imports for language or else every language will be included in your build (adds ~800 kb)
import { addLocaleData } from 'react-intl';

// need Intl polyfill, Intl not supported in Safari
import Intl from 'intl';
global.Intl = Intl;

// use this to allow nested messages, taken from docs:
// https://github.com/yahoo/react-intl/wiki/Upgrade-Guide#flatten-messages-object
function flattenMessages(nestedMessages = {}, prefix = '') {
return Object.keys(nestedMessages).reduce((messages, key) => {
const value = nestedMessages[key];
const prefixedKey = prefix ? `${prefix}.${key}` : key;

if (typeof value === 'string') {
messages[prefixedKey] = value; // eslint-disable-line no-param-reassign
} else {
Object.assign(messages, flattenMessages(value, prefixedKey));
}

return messages;
}, {});
}

// bring in intl polyfill, react-intl, and app-specific language data
import 'intl/locale-data/jsonp/en';
import en from 'react-intl/locale-data/en';
import enData from './localizationData/en';
addLocaleData(en);
localizationData.en = enData;
localizationData.en.messages = flattenMessages(localizationData.en.messages);

import 'intl/locale-data/jsonp/fr';
import fr from 'react-intl/locale-data/fr';
import frData from './localizationData/fr';
addLocaleData(fr);
localizationData.fr = frData;
localizationData.fr.messages = flattenMessages(localizationData.fr.messages);
9 changes: 6 additions & 3 deletions client/App.js
Expand Up @@ -4,6 +4,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import IntlWrapper from './modules/Intl/IntlWrapper';

// Import Routes
import routes from './routes';
Expand All @@ -14,9 +15,11 @@ require('./main.css');
export default function App(props) {
return (
<Provider store={props.store}>
<Router history={browserHistory}>
{routes}
</Router>
<IntlWrapper>
<Router history={browserHistory}>
{routes}
</Router>
</IntlWrapper>
</Provider>
);
}
Expand Down
17 changes: 15 additions & 2 deletions client/modules/App/App.js
Expand Up @@ -12,6 +12,7 @@ import Footer from './components/Footer/Footer';

// Import Actions
import { toggleAddPost } from './AppActions';
import { switchLanguage } from '../../modules/Intl/IntlActions';

export class App extends Component {
constructor(props) {
Expand Down Expand Up @@ -47,7 +48,11 @@ export class App extends Component {
},
]}
/>
<Header toggleAddPost={this.toggleAddPostSection} />
<Header
switchLanguage={lang => this.props.dispatch(switchLanguage(lang))}
intl={this.props.intl}
toggleAddPost={this.toggleAddPostSection}
/>
<div className={styles.container}>
{this.props.children}
</div>
Expand All @@ -61,6 +66,14 @@ export class App extends Component {
App.propTypes = {
children: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};

export default connect()(App);
// Retrieve data from store as props
function mapStateToProps(store) {
return {
intl: store.intl,
};
}

export default connect(mapStateToProps)(App);
6 changes: 6 additions & 0 deletions client/modules/App/__tests__/App.spec.js
Expand Up @@ -4,13 +4,17 @@ import sinon from 'sinon';
import { shallow, mount } from 'enzyme';
import { App } from '../App';
import styles from '../App.css';
import { intlShape } from 'react-intl';
import { intl } from '../../../util/react-intl-test-helper';
import { toggleAddPost } from '../AppActions';

const intlProp = { ...intl, enabledLanguages: ['en', 'fr'] };
const children = <h1>Test</h1>;
const dispatch = sinon.spy();
const props = {
children,
dispatch,
intl: intlProp,
};

test('renders properly', t => {
Expand Down Expand Up @@ -42,9 +46,11 @@ test('calls componentDidMount', t => {
setRouteLeaveHook: sinon.stub(),
createHref: sinon.stub(),
},
intl,
},
childContextTypes: {
router: React.PropTypes.object,
intl: intlShape,
},
},
);
Expand Down
2 changes: 1 addition & 1 deletion client/modules/App/__tests__/Components/Footer.spec.js
@@ -1,7 +1,7 @@
import React from 'react';
import test from 'ava';
import { shallow } from 'enzyme';
import Footer from '../../components/Footer/Footer';
import { Footer } from '../../components/Footer/Footer';

test('renders the footer properly', t => {
const wrapper = shallow(
Expand Down
17 changes: 12 additions & 5 deletions client/modules/App/__tests__/Components/Header.spec.js
Expand Up @@ -2,22 +2,27 @@ import React from 'react';
import test from 'ava';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import Header from '../../components/Header/Header';
import { FormattedMessage } from 'react-intl';
import { Header } from '../../components/Header/Header';
import { intl } from '../../../../util/react-intl-test-helper';

const intlProp = { ...intl, enabledLanguages: ['en', 'fr'] };

test('renders the header properly', t => {
const router = {
isActive: sinon.stub().returns(true),
};
const wrapper = shallow(
<Header toggleAddPost={() => {}} />,
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={() => {}} />,
{
context: {
router,
intl,
},
}
);

t.regex(wrapper.find('Link').first().html(), /MERN Starter Blog/);
t.truthy(wrapper.find('Link').first().containsMatchingElement(<FormattedMessage id="siteTitle" />));
t.is(wrapper.find('a').length, 1);
});

Expand All @@ -26,10 +31,11 @@ test('doesn\'t add post in pages other than home', t => {
isActive: sinon.stub().returns(false),
};
const wrapper = shallow(
<Header toggleAddPost={() => {}} />,
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={() => {}} />,
{
context: {
router,
intl,
},
}
);
Expand All @@ -43,10 +49,11 @@ test('toggleAddPost called properly', t => {
};
const toggleAddPost = sinon.spy();
const wrapper = shallow(
<Header toggleAddPost={toggleAddPost} />,
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={toggleAddPost} />,
{
context: {
router,
intl,
},
}
);
Expand Down
5 changes: 3 additions & 2 deletions client/modules/App/components/Footer/Footer.js
@@ -1,16 +1,17 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';

// Import Style
import styles from './Footer.css';

// Import Images
import bg from '../../header-bk.png';

function Footer() {
export function Footer() {
return (
<div style={{ background: `#FFF url(${bg}) center` }} className={styles.footer}>
<p>&copy; 2016 &middot; Hashnode &middot; LinearBytes Inc.</p>
<p>We are on Twitter : <a href="https://twitter.com/@mern_io" target="_Blank">@mern_io</a></p>
<p><FormattedMessage id="twitterMessage" /> : <a href="https://twitter.com/@mern_io" target="_Blank">@mern_io</a></p>
</div>
);
}
Expand Down
27 changes: 27 additions & 0 deletions client/modules/App/components/Header/Header.css
Expand Up @@ -33,6 +33,33 @@
float: right;
}

.language-switcher {
background: rgba(0, 0, 0, 0.1);
}

.language-switcher ul {
list-style: none;
width: 980px;
margin: auto;
text-align: right;
}

.language-switcher li {
display: inline-block;
margin: 10px;
padding: 5px;
cursor: pointer;
color: #fff;
}

.language-switcher li:first-child {
color: rgba(255, 255, 255, 0.7);
}

.selected {
border-bottom: 1px solid #fff;
}

@media (max-width: 767px){
.add-post-button{
float: left;
Expand Down
19 changes: 16 additions & 3 deletions client/modules/App/components/Header/Header.js
@@ -1,19 +1,30 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { FormattedMessage } from 'react-intl';

// Import Style
import styles from './Header.css';

function Header(props, context) {
export function Header(props, context) {
const languageNodes = props.intl.enabledLanguages.map(
lang => <li key={lang} onClick={() => props.switchLanguage(lang)} className={lang === props.intl.locale ? styles.selected : ''}>{lang}</li>
);

return (
<div className={styles.header}>
<div className={styles['language-switcher']}>
<ul>
<li><FormattedMessage id="switchLanguage" /></li>
{languageNodes}
</ul>
</div>
<div className={styles.content}>
<h1 className={styles['site-title']}>
<Link to="/" >MERN Starter Blog</Link>
<Link to="/" ><FormattedMessage id="siteTitle" /></Link>
</h1>
{
context.router.isActive('/', true)
? <a className={styles['add-post-button']} href="#" onClick={props.toggleAddPost}>Add Post</a>
? <a className={styles['add-post-button']} href="#" onClick={props.toggleAddPost}><FormattedMessage id="addPost" /></a>
: null
}
</div>
Expand All @@ -27,6 +38,8 @@ Header.contextTypes = {

Header.propTypes = {
toggleAddPost: PropTypes.func.isRequired,
switchLanguage: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};

export default Header;
11 changes: 11 additions & 0 deletions client/modules/Intl/IntlActions.js
@@ -0,0 +1,11 @@
import { localizationData } from '../../../Intl/setup';

// Export Constants
export const SWITCH_LANGUAGE = 'SWITCH_LANGUAGE';

export function switchLanguage(newLang) {
return {
type: SWITCH_LANGUAGE,
...localizationData[newLang],
};
}

0 comments on commit 7ac297b

Please sign in to comment.