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:
parent
898f90218e
commit
e24db68f8b
19
examples/with-react-intl/.babelrc
Normal file
19
examples/with-react-intl/.babelrc
Normal 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
1
examples/with-react-intl/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
lang/.messages/
|
51
examples/with-react-intl/README.md
Normal file
51
examples/with-react-intl/README.md
Normal 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
|
27
examples/with-react-intl/components/Layout.js
Normal file
27
examples/with-react-intl/components/Layout.js
Normal 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>
|
||||
))
|
28
examples/with-react-intl/components/Nav.js
Normal file
28
examples/with-react-intl/components/Nav.js
Normal 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>
|
||||
)
|
44
examples/with-react-intl/components/PageWithIntl.js
Normal file
44
examples/with-react-intl/components/PageWithIntl.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
7
examples/with-react-intl/lang/en.json
Normal file
7
examples/with-react-intl/lang/en.json
Normal 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!"
|
||||
}
|
7
examples/with-react-intl/lang/fr.json
Normal file
7
examples/with-react-intl/lang/fr.json
Normal 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!"
|
||||
}
|
21
examples/with-react-intl/package.json
Normal file
21
examples/with-react-intl/package.json
Normal 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"
|
||||
}
|
31
examples/with-react-intl/pages/_document.js
Normal file
31
examples/with-react-intl/pages/_document.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
25
examples/with-react-intl/pages/about.js
Normal file
25
examples/with-react-intl/pages/about.js
Normal 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)
|
26
examples/with-react-intl/pages/index.js
Normal file
26
examples/with-react-intl/pages/index.js
Normal 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>
|
||||
))
|
19
examples/with-react-intl/scripts/default-lang.js
Normal file
19
examples/with-react-intl/scripts/default-lang.js
Normal 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')}"`)
|
53
examples/with-react-intl/server.js
Normal file
53
examples/with-react-intl/server.js
Normal 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')
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue