Skip to content

Commit e24db68

Browse files
ericftimneutkens
authored andcommitted
Add example app with React Intl (vercel#1055)
* Add example app with React Intl Fixes vercel#1022 * Update examples/with-react-intl/package.json to be consistent
1 parent 898f902 commit e24db68

File tree

14 files changed

+359
-0
lines changed

14 files changed

+359
-0
lines changed

examples/with-react-intl/.babelrc

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"presets": [
3+
"next/babel"
4+
],
5+
"env": {
6+
"development": {
7+
"plugins": [
8+
"react-intl"
9+
]
10+
},
11+
"production": {
12+
"plugins": [
13+
["react-intl", {
14+
"messagesDir": "lang/.messages/"
15+
}]
16+
]
17+
}
18+
}
19+
}

examples/with-react-intl/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lang/.messages/

examples/with-react-intl/README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Example app with [React Intl][]
2+
3+
## How to use
4+
5+
Download the example [or clone the repo](https://github.com/zeit/next.js.git):
6+
7+
```bash
8+
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-react-intl
9+
cd with-react-intl
10+
```
11+
12+
Install it and run:
13+
14+
```bash
15+
npm install
16+
npm run dev
17+
```
18+
19+
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
20+
21+
```bash
22+
now
23+
```
24+
25+
## The idea behind the example
26+
27+
This example app shows how to integrate [React Intl][] with Next.
28+
29+
### Features of this example app
30+
31+
- Server-side language negotiation
32+
- React Intl locale data loading via `pages/_document.js` customization
33+
- React Intl integration at Next page level via `pageWithIntl()` HOC
34+
- `<IntlProvider>` creation with `locale`, `messages`, and `initialNow` props
35+
- Default message extraction via `babel-plugin-react-intl` integration
36+
- Translation management via build script and customized Next server
37+
38+
### Translation Management
39+
40+
This app stores translations and default strings in the `lang/` dir. This dir has `.messages/` subdir which is where React Intl's Babel plugin outputs the default messages it extracts from the source code. The default messages (`en.json` in this example app) is also generated by the build script. This file can then be sent to a translation service to perform localization for the other locales the app should support.
41+
42+
The translated messages files that exist at `lang/*.json` are only used during production, and are automatically provided to the `<IntlProvider>`. During development the `defaultMessage`s defined in the source code are used. To prepare the example app for localization and production run the build script and start the server in production mode:
43+
44+
```
45+
$ npm run build
46+
$ npm start
47+
```
48+
49+
You can then switch your browser's language preferences to French and refresh the page to see the UI update accordingly.
50+
51+
[React Intl]: https://github.com/yahoo/react-intl
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react'
2+
import {defineMessages, injectIntl} from 'react-intl'
3+
import Head from 'next/head'
4+
import Nav from './Nav'
5+
6+
const messages = defineMessages({
7+
title: {
8+
id: 'title',
9+
defaultMessage: 'React Intl Next.js Example'
10+
}
11+
})
12+
13+
export default injectIntl(({intl, title, children}) => (
14+
<div>
15+
<Head>
16+
<meta name='viewport' content='width=device-width, initial-scale=1' />
17+
<title>{title || intl.formatMessage(messages.title)}</title>
18+
</Head>
19+
20+
<header>
21+
<Nav />
22+
</header>
23+
24+
{children}
25+
26+
</div>
27+
))
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react'
2+
import {FormattedMessage} from 'react-intl'
3+
import Link from 'next/link'
4+
5+
export default () => (
6+
<nav>
7+
<li>
8+
<Link href='/'>
9+
<a><FormattedMessage id='nav.home' defaultMessage='Home' /></a>
10+
</Link>
11+
</li>
12+
<li>
13+
<Link href='/about'>
14+
<a><FormattedMessage id='nav.about' defaultMessage='About' /></a>
15+
</Link>
16+
</li>
17+
18+
<style jsx>{`
19+
nav {
20+
display: flex;
21+
}
22+
li {
23+
list-style: none;
24+
margin-right: 1rem;
25+
}
26+
`}</style>
27+
</nav>
28+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, {Component} from 'react'
2+
import {IntlProvider, addLocaleData, injectIntl} from 'react-intl'
3+
4+
// Register React Intl's locale data for the user's locale in the browser. This
5+
// locale data was added to the page by `pages/_document.js`. This only happens
6+
// once, on initial page load in the browser.
7+
if (typeof window !== 'undefined' && window.ReactIntlLocaleData) {
8+
Object.keys(window.ReactIntlLocaleData).forEach((lang) => {
9+
addLocaleData(window.ReactIntlLocaleData[lang])
10+
})
11+
}
12+
13+
export default (Page) => {
14+
const IntlPage = injectIntl(Page)
15+
16+
return class PageWithIntl extends Component {
17+
static async getInitialProps (context) {
18+
let props
19+
if (typeof Page.getInitialProps === 'function') {
20+
props = await Page.getInitialProps(context)
21+
}
22+
23+
// Get the `locale` and `messages` from the request object on the server.
24+
// In the browser, use the same values that the server serialized.
25+
const {req} = context
26+
const {locale, messages} = req || window.__NEXT_DATA__.props
27+
28+
// Always update the current time on page load/transition because the
29+
// <IntlProvider> will be a new instance even with pushState routing.
30+
const now = Date.now()
31+
32+
return {...props, locale, messages, now}
33+
}
34+
35+
render () {
36+
const {locale, messages, now, ...props} = this.props
37+
return (
38+
<IntlProvider locale={locale} messages={messages} initialNow={now}>
39+
<IntlPage {...props} />
40+
</IntlProvider>
41+
)
42+
}
43+
}
44+
}

examples/with-react-intl/lang/en.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"title": "React Intl Next.js Example",
3+
"nav.home": "Home",
4+
"nav.about": "About",
5+
"description": "An example app integrating React Intl with Next.js",
6+
"greeting": "Hello, World!"
7+
}

examples/with-react-intl/lang/fr.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"title": "React Intl Next.js Exemple",
3+
"nav.home": "Accueil",
4+
"nav.about": "À propos de nous",
5+
"description": "Un exemple d'application intégrant React Intl avec Next.js",
6+
"greeting": "Bonjour le monde!"
7+
}

examples/with-react-intl/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "with-react-intl",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "node server.js",
6+
"build": "next build && node ./scripts/default-lang",
7+
"start": "NODE_ENV=production node server.js"
8+
},
9+
"dependencies": {
10+
"accepts": "^1.3.3",
11+
"babel-plugin-react-intl": "^2.3.1",
12+
"glob": "^7.1.1",
13+
"intl": "^1.2.5",
14+
"next": "^2.0.0-beta",
15+
"react": "^15.4.2",
16+
"react-dom": "^15.4.2",
17+
"react-intl": "^2.2.3"
18+
},
19+
"author": "",
20+
"license": "ISC"
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Document, {Head, Main, NextScript} from 'next/document'
2+
3+
// The document (which is SSR-only) needs to be customized to expose the locale
4+
// data for the user's locale for React Intl to work in the browser.
5+
export default class IntlDocument extends Document {
6+
static async getInitialProps (context) {
7+
const props = await super.getInitialProps(context)
8+
const {req: {localeDataScript}} = context
9+
return {
10+
...props,
11+
localeDataScript
12+
}
13+
}
14+
15+
render () {
16+
return (
17+
<html>
18+
<Head />
19+
<body>
20+
<Main />
21+
<script
22+
dangerouslySetInnerHTML={{
23+
__html: this.props.localeDataScript
24+
}}
25+
/>
26+
<NextScript />
27+
</body>
28+
</html>
29+
)
30+
}
31+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, {Component} from 'react'
2+
import {FormattedRelative} from 'react-intl'
3+
import pageWithIntl from '../components/PageWithIntl'
4+
import Layout from '../components/Layout'
5+
6+
class About extends Component {
7+
static async getInitialProps ({req}) {
8+
return {someDate: Date.now()}
9+
}
10+
11+
render () {
12+
return (
13+
<Layout>
14+
<p>
15+
<FormattedRelative
16+
value={this.props.someDate}
17+
updateInterval={1000}
18+
/>
19+
</p>
20+
</Layout>
21+
)
22+
}
23+
}
24+
25+
export default pageWithIntl(About)
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react'
2+
import {FormattedMessage, FormattedNumber, defineMessages} from 'react-intl'
3+
import Head from 'next/head'
4+
import pageWithIntl from '../components/PageWithIntl'
5+
import Layout from '../components/Layout'
6+
7+
const {description} = defineMessages({
8+
description: {
9+
id: 'description',
10+
defaultMessage: 'An example app integrating React Intl with Next.js'
11+
}
12+
})
13+
14+
export default pageWithIntl(({intl}) => (
15+
<Layout>
16+
<Head>
17+
<meta name='description' content={intl.formatMessage(description)} />
18+
</Head>
19+
<p>
20+
<FormattedMessage id='greeting' defaultMessage='Hello, World!' />
21+
</p>
22+
<p>
23+
<FormattedNumber value={1000} />
24+
</p>
25+
</Layout>
26+
))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const {readFileSync, writeFileSync} = require('fs')
2+
const {resolve} = require('path')
3+
const glob = require('glob')
4+
5+
const defaultMessages = glob.sync('./lang/.messages/**/*.json')
6+
.map((filename) => readFileSync(filename, 'utf8'))
7+
.map((file) => JSON.parse(file))
8+
.reduce((messages, descriptors) => {
9+
descriptors.forEach(({id, defaultMessage}) => {
10+
if (messages.hasOwnProperty(id)) {
11+
throw new Error(`Duplicate message id: ${id}`)
12+
}
13+
messages[id] = defaultMessage
14+
})
15+
return messages
16+
}, {})
17+
18+
writeFileSync('./lang/en.json', JSON.stringify(defaultMessages, null, 2))
19+
console.log(`> Wrote default messages to: "${resolve('./lang/en.json')}"`)

examples/with-react-intl/server.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Polyfill Node with `Intl` that has data for all locales.
2+
// See: https://formatjs.io/guides/runtime-environments/#server
3+
const IntlPolyfill = require('intl')
4+
Intl.NumberFormat = IntlPolyfill.NumberFormat
5+
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat
6+
7+
const {readFileSync} = require('fs')
8+
const {basename} = require('path')
9+
const {createServer} = require('http')
10+
const accepts = require('accepts')
11+
const glob = require('glob')
12+
const next = require('next')
13+
14+
const dev = process.env.NODE_ENV !== 'production'
15+
const app = next({dev})
16+
const handle = app.getRequestHandler()
17+
18+
// Get the supported languages by looking for translations in the `lang/` dir.
19+
const languages = glob.sync('./lang/*.json').map((f) => basename(f, '.json'))
20+
21+
// We need to expose React Intl's locale data on the request for the user's
22+
// locale. This function will also cache the scripts by lang in memory.
23+
const localeDataCache = new Map()
24+
const getLocaleDataScript = (locale) => {
25+
const lang = locale.split('-')[0]
26+
if (!localeDataCache.has(lang)) {
27+
const localeDataFile = require.resolve(`react-intl/locale-data/${lang}`)
28+
const localeDataScript = readFileSync(localeDataFile, 'utf8')
29+
localeDataCache.set(lang, localeDataScript)
30+
}
31+
return localeDataCache.get(lang)
32+
}
33+
34+
// We need to load and expose the translations on the request for the user's
35+
// locale. These will only be used in production, in dev the `defaultMessage` in
36+
// each message description in the source code will be used.
37+
const getMessages = (locale) => {
38+
return require(`./lang/${locale}.json`)
39+
}
40+
41+
app.prepare().then(() => {
42+
createServer((req, res) => {
43+
const accept = accepts(req)
44+
const locale = accept.language(dev ? ['en'] : languages)
45+
req.locale = locale
46+
req.localeDataScript = getLocaleDataScript(locale)
47+
req.messages = dev ? {} : getMessages(locale)
48+
handle(req, res)
49+
}).listen(3000, (err) => {
50+
if (err) throw err
51+
console.log('> Read on http://localhost:3000')
52+
})
53+
})

0 commit comments

Comments
 (0)