1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Add example app with React Intl (#1055)

* Add example app with React Intl

Fixes #1022

* Update examples/with-react-intl/package.json to be consistent
This commit is contained in:
Eric Ferraiuolo 2017-02-24 16:45:18 -05:00 committed by Tim Neutkens
parent 898f90218e
commit e24db68f8b
14 changed files with 359 additions and 0 deletions

View file

@ -0,0 +1,19 @@
{
"presets": [
"next/babel"
],
"env": {
"development": {
"plugins": [
"react-intl"
]
},
"production": {
"plugins": [
["react-intl", {
"messagesDir": "lang/.messages/"
}]
]
}
}
}

1
examples/with-react-intl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
lang/.messages/

View file

@ -0,0 +1,51 @@
# Example app with [React Intl][]
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js.git):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-react-intl
cd with-react-intl
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example app shows how to integrate [React Intl][] with Next.
### Features of this example app
- Server-side language negotiation
- React Intl locale data loading via `pages/_document.js` customization
- React Intl integration at Next page level via `pageWithIntl()` HOC
- `<IntlProvider>` creation with `locale`, `messages`, and `initialNow` props
- Default message extraction via `babel-plugin-react-intl` integration
- Translation management via build script and customized Next server
### Translation Management
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.
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:
```
$ npm run build
$ npm start
```
You can then switch your browser's language preferences to French and refresh the page to see the UI update accordingly.
[React Intl]: https://github.com/yahoo/react-intl

View file

@ -0,0 +1,27 @@
import React from 'react'
import {defineMessages, injectIntl} from 'react-intl'
import Head from 'next/head'
import Nav from './Nav'
const messages = defineMessages({
title: {
id: 'title',
defaultMessage: 'React Intl Next.js Example'
}
})
export default injectIntl(({intl, title, children}) => (
<div>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<title>{title || intl.formatMessage(messages.title)}</title>
</Head>
<header>
<Nav />
</header>
{children}
</div>
))

View file

@ -0,0 +1,28 @@
import React from 'react'
import {FormattedMessage} from 'react-intl'
import Link from 'next/link'
export default () => (
<nav>
<li>
<Link href='/'>
<a><FormattedMessage id='nav.home' defaultMessage='Home' /></a>
</Link>
</li>
<li>
<Link href='/about'>
<a><FormattedMessage id='nav.about' defaultMessage='About' /></a>
</Link>
</li>
<style jsx>{`
nav {
display: flex;
}
li {
list-style: none;
margin-right: 1rem;
}
`}</style>
</nav>
)

View file

@ -0,0 +1,44 @@
import React, {Component} from 'react'
import {IntlProvider, addLocaleData, injectIntl} from 'react-intl'
// Register React Intl's locale data for the user's locale in the browser. This
// locale data was added to the page by `pages/_document.js`. This only happens
// once, on initial page load in the browser.
if (typeof window !== 'undefined' && window.ReactIntlLocaleData) {
Object.keys(window.ReactIntlLocaleData).forEach((lang) => {
addLocaleData(window.ReactIntlLocaleData[lang])
})
}
export default (Page) => {
const IntlPage = injectIntl(Page)
return class PageWithIntl extends Component {
static async getInitialProps (context) {
let props
if (typeof Page.getInitialProps === 'function') {
props = await Page.getInitialProps(context)
}
// Get the `locale` and `messages` from the request object on the server.
// In the browser, use the same values that the server serialized.
const {req} = context
const {locale, messages} = req || window.__NEXT_DATA__.props
// Always update the current time on page load/transition because the
// <IntlProvider> will be a new instance even with pushState routing.
const now = Date.now()
return {...props, locale, messages, now}
}
render () {
const {locale, messages, now, ...props} = this.props
return (
<IntlProvider locale={locale} messages={messages} initialNow={now}>
<IntlPage {...props} />
</IntlProvider>
)
}
}
}

View file

@ -0,0 +1,7 @@
{
"title": "React Intl Next.js Example",
"nav.home": "Home",
"nav.about": "About",
"description": "An example app integrating React Intl with Next.js",
"greeting": "Hello, World!"
}

View file

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

View file

@ -0,0 +1,21 @@
{
"name": "with-react-intl",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build && node ./scripts/default-lang",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"accepts": "^1.3.3",
"babel-plugin-react-intl": "^2.3.1",
"glob": "^7.1.1",
"intl": "^1.2.5",
"next": "^2.0.0-beta",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-intl": "^2.2.3"
},
"author": "",
"license": "ISC"
}

View file

@ -0,0 +1,31 @@
import Document, {Head, Main, NextScript} from 'next/document'
// The document (which is SSR-only) needs to be customized to expose the locale
// data for the user's locale for React Intl to work in the browser.
export default class IntlDocument extends Document {
static async getInitialProps (context) {
const props = await super.getInitialProps(context)
const {req: {localeDataScript}} = context
return {
...props,
localeDataScript
}
}
render () {
return (
<html>
<Head />
<body>
<Main />
<script
dangerouslySetInnerHTML={{
__html: this.props.localeDataScript
}}
/>
<NextScript />
</body>
</html>
)
}
}

View file

@ -0,0 +1,25 @@
import React, {Component} from 'react'
import {FormattedRelative} from 'react-intl'
import pageWithIntl from '../components/PageWithIntl'
import Layout from '../components/Layout'
class About extends Component {
static async getInitialProps ({req}) {
return {someDate: Date.now()}
}
render () {
return (
<Layout>
<p>
<FormattedRelative
value={this.props.someDate}
updateInterval={1000}
/>
</p>
</Layout>
)
}
}
export default pageWithIntl(About)

View file

@ -0,0 +1,26 @@
import React from 'react'
import {FormattedMessage, FormattedNumber, defineMessages} from 'react-intl'
import Head from 'next/head'
import pageWithIntl from '../components/PageWithIntl'
import Layout from '../components/Layout'
const {description} = defineMessages({
description: {
id: 'description',
defaultMessage: 'An example app integrating React Intl with Next.js'
}
})
export default pageWithIntl(({intl}) => (
<Layout>
<Head>
<meta name='description' content={intl.formatMessage(description)} />
</Head>
<p>
<FormattedMessage id='greeting' defaultMessage='Hello, World!' />
</p>
<p>
<FormattedNumber value={1000} />
</p>
</Layout>
))

View file

@ -0,0 +1,19 @@
const {readFileSync, writeFileSync} = require('fs')
const {resolve} = require('path')
const glob = require('glob')
const defaultMessages = glob.sync('./lang/.messages/**/*.json')
.map((filename) => readFileSync(filename, 'utf8'))
.map((file) => JSON.parse(file))
.reduce((messages, descriptors) => {
descriptors.forEach(({id, defaultMessage}) => {
if (messages.hasOwnProperty(id)) {
throw new Error(`Duplicate message id: ${id}`)
}
messages[id] = defaultMessage
})
return messages
}, {})
writeFileSync('./lang/en.json', JSON.stringify(defaultMessages, null, 2))
console.log(`> Wrote default messages to: "${resolve('./lang/en.json')}"`)

View file

@ -0,0 +1,53 @@
// Polyfill Node with `Intl` that has data for all locales.
// See: https://formatjs.io/guides/runtime-environments/#server
const IntlPolyfill = require('intl')
Intl.NumberFormat = IntlPolyfill.NumberFormat
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat
const {readFileSync} = require('fs')
const {basename} = require('path')
const {createServer} = require('http')
const accepts = require('accepts')
const glob = require('glob')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({dev})
const handle = app.getRequestHandler()
// Get the supported languages by looking for translations in the `lang/` dir.
const languages = glob.sync('./lang/*.json').map((f) => basename(f, '.json'))
// We need to expose React Intl's locale data on the request for the user's
// locale. This function will also cache the scripts by lang in memory.
const localeDataCache = new Map()
const getLocaleDataScript = (locale) => {
const lang = locale.split('-')[0]
if (!localeDataCache.has(lang)) {
const localeDataFile = require.resolve(`react-intl/locale-data/${lang}`)
const localeDataScript = readFileSync(localeDataFile, 'utf8')
localeDataCache.set(lang, localeDataScript)
}
return localeDataCache.get(lang)
}
// We need to load and expose the translations on the request for the user's
// locale. These will only be used in production, in dev the `defaultMessage` in
// each message description in the source code will be used.
const getMessages = (locale) => {
return require(`./lang/${locale}.json`)
}
app.prepare().then(() => {
createServer((req, res) => {
const accept = accepts(req)
const locale = accept.language(dev ? ['en'] : languages)
req.locale = locale
req.localeDataScript = getLocaleDataScript(locale)
req.messages = dev ? {} : getMessages(locale)
handle(req, res)
}).listen(3000, (err) => {
if (err) throw err
console.log('> Read on http://localhost:3000')
})
})