Skip to content

Commit 7ac297b

Browse files
authored
Add react-intl (#166 & #163)
* 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
1 parent 494a713 commit 7ac297b

File tree

26 files changed

+351
-69
lines changed

26 files changed

+351
-69
lines changed

.babelrc

-11
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,6 @@
44
"env": {
55
"production": {
66
"presets": ["es2015", "react", "react-optimize", "es2015-native-modules", "stage-0"]
7-
},
8-
"test": {
9-
"plugins": [
10-
[
11-
"babel-plugin-webpack-loaders",
12-
{
13-
"config": "${CONFIG}",
14-
"verbose": false
15-
}
16-
]
17-
]
187
}
198
}
209
}

Intl/localizationData/en.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export default {
2+
locale: 'en',
3+
messages: {
4+
siteTitle: 'MERN Starter Blog',
5+
addPost: 'Add Post',
6+
switchLanguage: 'Switch Language',
7+
twitterMessage: 'We are on Twitter',
8+
by: 'By',
9+
deletePost: 'Delete Post',
10+
createNewPost: 'Create new post',
11+
authorName: 'Author\'s Name',
12+
postTitle: 'Post Title',
13+
postContent: 'Post Content',
14+
submit: 'Submit',
15+
comment: `user {name} {value, plural,
16+
=0 {does not have any comments}
17+
=1 {has # comment}
18+
other {has # comments}
19+
}`,
20+
HTMLComment: `user <b style='font-weight: bold'>{name} </b> {value, plural,
21+
=0 {does not have <i style='font-style: italic'>any</i> comments}
22+
=1 {has <i style='font-style: italic'>#</i> comment}
23+
other {has <i style='font-style: italic'>#</i> comments}
24+
}`,
25+
nestedDateComment: `user {name} {value, plural,
26+
=0 {does not have any comments}
27+
=1 {has # comment}
28+
other {has # comments}
29+
} as of {date}`,
30+
},
31+
};

Intl/localizationData/fr.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export default {
2+
locale: 'fr',
3+
messages: {
4+
siteTitle: 'MERN blog de démarrage',
5+
addPost: 'Ajouter Poster',
6+
switchLanguage: 'Changer de langue',
7+
twitterMessage: 'Nous sommes sur Twitter',
8+
by: 'Par',
9+
deletePost: 'Supprimer le message',
10+
createNewPost: 'Créer un nouveau message',
11+
authorName: 'Nom de l\'auteur',
12+
postTitle: 'Titre de l\'article',
13+
postContent: 'Contenu après',
14+
submit: 'Soumettre',
15+
comment: `user {name} {value, plural,
16+
=0 {does not have any comments}
17+
=1 {has # comment}
18+
other {has # comments}
19+
} (in real app this would be translated to French)`,
20+
HTMLComment: `user <b style='font-weight: bold'>{name} </b> {value, plural,
21+
=0 {does not have <i style='font-style: italic'>any</i> comments}
22+
=1 {has <i style='font-style: italic'>#</i> comment}
23+
other {has <i style='font-style: italic'>#</i> comments}
24+
} (in real app this would be translated to French)`,
25+
nestedDateComment: `user {name} {value, plural,
26+
=0 {does not have any comments}
27+
=1 {has # comment}
28+
other {has # comments}
29+
} as of {date} (in real app this would be translated to French)`,
30+
},
31+
};

Intl/setup.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// list of available languages
2+
export const enabledLanguages = [
3+
'en',
4+
'fr',
5+
];
6+
7+
// this object will have language-specific data added to it which will be placed in the state when that language is active
8+
// if localization data get to big, stop importing in all languages and switch to using API requests to load upon switching languages
9+
export const localizationData = {};
10+
11+
// here you bring in 'intl' browser polyfill and language-specific polyfills
12+
// (needed as safari doesn't have native intl: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)
13+
// as well as react-intl's language-specific data
14+
// be sure to use static imports for language or else every language will be included in your build (adds ~800 kb)
15+
import { addLocaleData } from 'react-intl';
16+
17+
// need Intl polyfill, Intl not supported in Safari
18+
import Intl from 'intl';
19+
global.Intl = Intl;
20+
21+
// use this to allow nested messages, taken from docs:
22+
// https://github.com/yahoo/react-intl/wiki/Upgrade-Guide#flatten-messages-object
23+
function flattenMessages(nestedMessages = {}, prefix = '') {
24+
return Object.keys(nestedMessages).reduce((messages, key) => {
25+
const value = nestedMessages[key];
26+
const prefixedKey = prefix ? `${prefix}.${key}` : key;
27+
28+
if (typeof value === 'string') {
29+
messages[prefixedKey] = value; // eslint-disable-line no-param-reassign
30+
} else {
31+
Object.assign(messages, flattenMessages(value, prefixedKey));
32+
}
33+
34+
return messages;
35+
}, {});
36+
}
37+
38+
// bring in intl polyfill, react-intl, and app-specific language data
39+
import 'intl/locale-data/jsonp/en';
40+
import en from 'react-intl/locale-data/en';
41+
import enData from './localizationData/en';
42+
addLocaleData(en);
43+
localizationData.en = enData;
44+
localizationData.en.messages = flattenMessages(localizationData.en.messages);
45+
46+
import 'intl/locale-data/jsonp/fr';
47+
import fr from 'react-intl/locale-data/fr';
48+
import frData from './localizationData/fr';
49+
addLocaleData(fr);
50+
localizationData.fr = frData;
51+
localizationData.fr.messages = flattenMessages(localizationData.fr.messages);

client/App.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import React from 'react';
55
import { Provider } from 'react-redux';
66
import { Router, browserHistory } from 'react-router';
7+
import IntlWrapper from './modules/Intl/IntlWrapper';
78

89
// Import Routes
910
import routes from './routes';
@@ -14,9 +15,11 @@ require('./main.css');
1415
export default function App(props) {
1516
return (
1617
<Provider store={props.store}>
17-
<Router history={browserHistory}>
18-
{routes}
19-
</Router>
18+
<IntlWrapper>
19+
<Router history={browserHistory}>
20+
{routes}
21+
</Router>
22+
</IntlWrapper>
2023
</Provider>
2124
);
2225
}

client/modules/App/App.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Footer from './components/Footer/Footer';
1212

1313
// Import Actions
1414
import { toggleAddPost } from './AppActions';
15+
import { switchLanguage } from '../../modules/Intl/IntlActions';
1516

1617
export class App extends Component {
1718
constructor(props) {
@@ -47,7 +48,11 @@ export class App extends Component {
4748
},
4849
]}
4950
/>
50-
<Header toggleAddPost={this.toggleAddPostSection} />
51+
<Header
52+
switchLanguage={lang => this.props.dispatch(switchLanguage(lang))}
53+
intl={this.props.intl}
54+
toggleAddPost={this.toggleAddPostSection}
55+
/>
5156
<div className={styles.container}>
5257
{this.props.children}
5358
</div>
@@ -61,6 +66,14 @@ export class App extends Component {
6166
App.propTypes = {
6267
children: PropTypes.object.isRequired,
6368
dispatch: PropTypes.func.isRequired,
69+
intl: PropTypes.object.isRequired,
6470
};
6571

66-
export default connect()(App);
72+
// Retrieve data from store as props
73+
function mapStateToProps(store) {
74+
return {
75+
intl: store.intl,
76+
};
77+
}
78+
79+
export default connect(mapStateToProps)(App);

client/modules/App/__tests__/App.spec.js

+6
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import sinon from 'sinon';
44
import { shallow, mount } from 'enzyme';
55
import { App } from '../App';
66
import styles from '../App.css';
7+
import { intlShape } from 'react-intl';
8+
import { intl } from '../../../util/react-intl-test-helper';
79
import { toggleAddPost } from '../AppActions';
810

11+
const intlProp = { ...intl, enabledLanguages: ['en', 'fr'] };
912
const children = <h1>Test</h1>;
1013
const dispatch = sinon.spy();
1114
const props = {
1215
children,
1316
dispatch,
17+
intl: intlProp,
1418
};
1519

1620
test('renders properly', t => {
@@ -42,9 +46,11 @@ test('calls componentDidMount', t => {
4246
setRouteLeaveHook: sinon.stub(),
4347
createHref: sinon.stub(),
4448
},
49+
intl,
4550
},
4651
childContextTypes: {
4752
router: React.PropTypes.object,
53+
intl: intlShape,
4854
},
4955
},
5056
);

client/modules/App/__tests__/Components/Footer.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import test from 'ava';
33
import { shallow } from 'enzyme';
4-
import Footer from '../../components/Footer/Footer';
4+
import { Footer } from '../../components/Footer/Footer';
55

66
test('renders the footer properly', t => {
77
const wrapper = shallow(

client/modules/App/__tests__/Components/Header.spec.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@ import React from 'react';
22
import test from 'ava';
33
import sinon from 'sinon';
44
import { shallow } from 'enzyme';
5-
import Header from '../../components/Header/Header';
5+
import { FormattedMessage } from 'react-intl';
6+
import { Header } from '../../components/Header/Header';
7+
import { intl } from '../../../../util/react-intl-test-helper';
8+
9+
const intlProp = { ...intl, enabledLanguages: ['en', 'fr'] };
610

711
test('renders the header properly', t => {
812
const router = {
913
isActive: sinon.stub().returns(true),
1014
};
1115
const wrapper = shallow(
12-
<Header toggleAddPost={() => {}} />,
16+
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={() => {}} />,
1317
{
1418
context: {
1519
router,
20+
intl,
1621
},
1722
}
1823
);
1924

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

@@ -26,10 +31,11 @@ test('doesn\'t add post in pages other than home', t => {
2631
isActive: sinon.stub().returns(false),
2732
};
2833
const wrapper = shallow(
29-
<Header toggleAddPost={() => {}} />,
34+
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={() => {}} />,
3035
{
3136
context: {
3237
router,
38+
intl,
3339
},
3440
}
3541
);
@@ -43,10 +49,11 @@ test('toggleAddPost called properly', t => {
4349
};
4450
const toggleAddPost = sinon.spy();
4551
const wrapper = shallow(
46-
<Header toggleAddPost={toggleAddPost} />,
52+
<Header switchLanguage={() => {}} intl={intlProp} toggleAddPost={toggleAddPost} />,
4753
{
4854
context: {
4955
router,
56+
intl,
5057
},
5158
}
5259
);

client/modules/App/components/Footer/Footer.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import React from 'react';
2+
import { FormattedMessage } from 'react-intl';
23

34
// Import Style
45
import styles from './Footer.css';
56

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

9-
function Footer() {
10+
export function Footer() {
1011
return (
1112
<div style={{ background: `#FFF url(${bg}) center` }} className={styles.footer}>
1213
<p>&copy; 2016 &middot; Hashnode &middot; LinearBytes Inc.</p>
13-
<p>We are on Twitter : <a href="https://twitter.com/@mern_io" target="_Blank">@mern_io</a></p>
14+
<p><FormattedMessage id="twitterMessage" /> : <a href="https://twitter.com/@mern_io" target="_Blank">@mern_io</a></p>
1415
</div>
1516
);
1617
}

client/modules/App/components/Header/Header.css

+27
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,33 @@
3333
float: right;
3434
}
3535

36+
.language-switcher {
37+
background: rgba(0, 0, 0, 0.1);
38+
}
39+
40+
.language-switcher ul {
41+
list-style: none;
42+
width: 980px;
43+
margin: auto;
44+
text-align: right;
45+
}
46+
47+
.language-switcher li {
48+
display: inline-block;
49+
margin: 10px;
50+
padding: 5px;
51+
cursor: pointer;
52+
color: #fff;
53+
}
54+
55+
.language-switcher li:first-child {
56+
color: rgba(255, 255, 255, 0.7);
57+
}
58+
59+
.selected {
60+
border-bottom: 1px solid #fff;
61+
}
62+
3663
@media (max-width: 767px){
3764
.add-post-button{
3865
float: left;

client/modules/App/components/Header/Header.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import React, { PropTypes } from 'react';
22
import { Link } from 'react-router';
3+
import { FormattedMessage } from 'react-intl';
34

45
// Import Style
56
import styles from './Header.css';
67

7-
function Header(props, context) {
8+
export function Header(props, context) {
9+
const languageNodes = props.intl.enabledLanguages.map(
10+
lang => <li key={lang} onClick={() => props.switchLanguage(lang)} className={lang === props.intl.locale ? styles.selected : ''}>{lang}</li>
11+
);
12+
813
return (
914
<div className={styles.header}>
15+
<div className={styles['language-switcher']}>
16+
<ul>
17+
<li><FormattedMessage id="switchLanguage" /></li>
18+
{languageNodes}
19+
</ul>
20+
</div>
1021
<div className={styles.content}>
1122
<h1 className={styles['site-title']}>
12-
<Link to="/" >MERN Starter Blog</Link>
23+
<Link to="/" ><FormattedMessage id="siteTitle" /></Link>
1324
</h1>
1425
{
1526
context.router.isActive('/', true)
16-
? <a className={styles['add-post-button']} href="#" onClick={props.toggleAddPost}>Add Post</a>
27+
? <a className={styles['add-post-button']} href="#" onClick={props.toggleAddPost}><FormattedMessage id="addPost" /></a>
1728
: null
1829
}
1930
</div>
@@ -27,6 +38,8 @@ Header.contextTypes = {
2738

2839
Header.propTypes = {
2940
toggleAddPost: PropTypes.func.isRequired,
41+
switchLanguage: PropTypes.func.isRequired,
42+
intl: PropTypes.object.isRequired,
3043
};
3144

3245
export default Header;

client/modules/Intl/IntlActions.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { localizationData } from '../../../Intl/setup';
2+
3+
// Export Constants
4+
export const SWITCH_LANGUAGE = 'SWITCH_LANGUAGE';
5+
6+
export function switchLanguage(newLang) {
7+
return {
8+
type: SWITCH_LANGUAGE,
9+
...localizationData[newLang],
10+
};
11+
}

0 commit comments

Comments
 (0)