-
Notifications
You must be signed in to change notification settings - Fork 27k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add example app with React Intl #1055
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"presets": [ | ||
"next/babel" | ||
], | ||
"env": { | ||
"development": { | ||
"plugins": [ | ||
"react-intl" | ||
] | ||
}, | ||
"production": { | ||
"plugins": [ | ||
["react-intl", { | ||
"messagesDir": "lang/.messages/" | ||
}] | ||
] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
lang/.messages/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')}"`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
}) | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is
window.ReactIntlLocaleData
created?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the locale data modules; e.g. https://unpkg.com/[email protected]/locale-data/en.js
The server selects one React Intl's locale data scripts and inlines it on the page. Since the locale data is packaged as a UMD module, in the browser it creates the
window.ReactIntlLocaleData
global.More details here: https://github.com/yahoo/react-intl/wiki#locale-data-in-browsers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing locale data around like this seems contrived 🤔 As I commented in formatjs/formatjs#731 (comment)
Is there no way for
getInitialProps
to return the localeData and simply pass that toaddLocaleData
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are several issues with Next at play here:
page/_document.js
is bundled with Webpack, and errors our if you try to usefs
or any other built-in Node modules even though it only runs on the client.The
__NEXT_DATA__
is serialized usingJSON.stringify()
. This means thatgetInitialProps()
must return an object that's JSON-serializable, and React Intl's locale data isn't JSON-serializable since it contains functions for pluralization.Calling
addLocaleData()
on every page transition wouldn't be ideal. It's best to only call this function one time.It's possible to address 1 and 2 of these issues in Next, and I'd be happy to rework example once they are changed, but as it stands now there isn't a good way around it.
For 2,
serialize-javascript
can be used which does support serializing functions. Changing Next to use this instead of rawJSON.stringify()
would remove the need for this example to use a custompages/_document.js
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the details 🙂
eval
)? Right now the intl code is spread all around and that's hardly maintainable.serverRendered: !process.browser
in your getInitialProps and wrap theaddLocaleData
inif (props.serverRendered)
?: https://github.com/relatenow/relate/blob/ee51f1dd6990177ff0924b1c0d78fddfd23aa564/hocs/withIntl.js#L43There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case
dangerouslySetInnerHTML
is being used only on the server and it's just creating an inline<script>
in the HMTL response. Your idea of usingeval()
would be running on the client would conflict with CSP; discussion here: #256How are you doing HTTP content negotiation for choosing the locale on both the server and client?
I encourage people to who have apps that render on both the server and client to do the content negotiation and choose the correct locale server-side, and have the client use the locale the server decided on.
The main issue I ran into when trying to do this type of content negotiation and locale data loading in the HOC is that Webpack would bundle all locale data, all translated messages, and npm packages that were only meant to run on the client. I think the only way to keep everything in the HOC would involve i18n related stuff in a custom webpack config file — and I don't know what that would look like, but maybe you or others do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I'm not familiar with CSP, will try to read more on this.
For detecting the browser locale I used
express-request-language
, then store it in the session, and allow the user to customise it (server-side).For co-locating the code a bit more, I just managed to load the message files from within the HOC (it does involve a slight webpack tweak): https://github.com/relatenow/relate/blob/master/hocs/withIntl.js#L25
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just updated to use the
accept-language
module to better co-locate the code: sedubois/relate@5c9d95aThis means that I don't need a custom server at all to use react-intl at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sedubois do you want to send a PR to my branch with your suggested changes? Then I can merge your commits into this branch/PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ericf ok I can try to send something, maybe tomorrow (not sure if have time).