diff --git a/examples/with-react-intl/.babelrc b/examples/with-react-intl/.babelrc new file mode 100644 index 0000000000..ece588bc02 --- /dev/null +++ b/examples/with-react-intl/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": ["next/babel"], + "plugins": [ + ["babel-plugin-react-intl", { + "ast": true, + "idInterpolationPattern": "[sha512:contenthash:base64:6]", + "extractFromFormatMessageCall": true + }] + ] + } \ No newline at end of file diff --git a/examples/with-react-intl/.gitignore b/examples/with-react-intl/.gitignore index eda331b4cb..b26bddaa72 100644 --- a/examples/with-react-intl/.gitignore +++ b/examples/with-react-intl/.gitignore @@ -33,3 +33,4 @@ yarn-error.log* # vercel .vercel +compiled-lang \ No newline at end of file diff --git a/examples/with-react-intl/.vscode/settings.json b/examples/with-react-intl/.vscode/settings.json new file mode 100644 index 0000000000..ad92582bd0 --- /dev/null +++ b/examples/with-react-intl/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/examples/with-react-intl/README.md b/examples/with-react-intl/README.md index 7cc945bae9..9160207442 100644 --- a/examples/with-react-intl/README.md +++ b/examples/with-react-intl/README.md @@ -1,11 +1,9 @@ # Example app with [React Intl][] -This example app shows how to integrate [React Intl][] with Next. +This example app shows how to integrate [React Intl][] with Next.js. ## How to use -### Using `create-next-app` - Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: ```bash @@ -14,39 +12,21 @@ npx create-next-app --example with-react-intl with-react-intl-app yarn create next-app --example with-react-intl with-react-intl-app ``` -### Download manually - -Download the example: - -```bash -curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-react-intl -cd with-react-intl -``` - -Install it and run: - -```bash -npm install -npm run dev -# or -yarn -yarn dev -``` - Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). -### Features of this example app +## Features of this example app - Server-side language negotiation - React Intl locale data loading via `pages/_document.js` customization - React Intl integration with [custom App](https://github.com/vercel/next.js#custom-app) component - `` creation with `locale`, `messages` props -- Default message extraction via `babel-plugin-react-intl` integration +- Default message extraction via `@formatjs/cli` integration +- Pre-compile messages into AST with `babel-plugin-react-intl` for performance - 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. +This app stores translations and default strings in the `lang/` dir. 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 ``. 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: @@ -57,21 +37,4 @@ $ npm start You can then switch your browser's language preferences to French and refresh the page to see the UI update accordingly. -### FormattedHTMLMessage support (react-intl pre-v4) - -Out of the box, this example does not support the use of the `FormattedHTMLMessage` component on the server due to `DOMParser` not being present in a Node environment. -This functionality is deprecated and has been removed as of react-intl 4.0 -If you still want to enable this feature, you should install a `DOMParser` implementation (e.g. `xmldom` or `jsdom`) and enable the polyfill in `server.js`: - -```js -// Polyfill Node with `DOMParser` required by formatjs. -// See: https://github.com/vercel/next.js/issues/10533 -const { DOMParser } = require('xmldom') -global.DOMParser = DOMParser -``` - -[react intl]: https://github.com/yahoo/react-intl - -### Transpile react-intl - -According to [react-intl docs](https://github.com/formatjs/react-intl/blob/53f2c826c7b1e50ad37215ce46b5e1c6f5d142cc/docs/Getting-Started.md#esm-build), react-intl and its underlying libraries must be transpiled to support older browsers (eg IE11). This is done by [next-transpile-modules](https://www.npmjs.com/package/next-transpile-modules) in next.config.js. +[react intl]: https://formatjs.io diff --git a/examples/with-react-intl/components/Layout.js b/examples/with-react-intl/components/Layout.js deleted file mode 100644 index 79f6933240..0000000000 --- a/examples/with-react-intl/components/Layout.js +++ /dev/null @@ -1,29 +0,0 @@ -import { defineMessages, useIntl } 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 function Layout({ title, children }) { - const intl = useIntl() - - return ( -
- - - {title || intl.formatMessage(messages.title)} - - -
-
- - {children} -
- ) -} diff --git a/examples/with-react-intl/components/Layout.tsx b/examples/with-react-intl/components/Layout.tsx new file mode 100644 index 0000000000..1a1f976f71 --- /dev/null +++ b/examples/with-react-intl/components/Layout.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import {useIntl} from 'react-intl'; +import Head from 'next/head'; +import Nav from './Nav'; + +export default function Layout({title, children}) { + const intl = useIntl(); + + return ( +
+ + + + {title || + intl.formatMessage({ + defaultMessage: 'React Intl Next.js Example', + })} + + + +
+
+ + {children} +
+ ); +} diff --git a/examples/with-react-intl/components/Nav.js b/examples/with-react-intl/components/Nav.tsx similarity index 65% rename from examples/with-react-intl/components/Nav.js rename to examples/with-react-intl/components/Nav.tsx index 4268db6a55..84032cb359 100644 --- a/examples/with-react-intl/components/Nav.js +++ b/examples/with-react-intl/components/Nav.tsx @@ -1,5 +1,6 @@ -import { FormattedMessage } from 'react-intl' -import Link from 'next/link' +import * as React from 'react'; +import {FormattedMessage} from 'react-intl'; +import Link from 'next/link'; export default function Nav() { return ( @@ -7,14 +8,14 @@ export default function Nav() {
  • - +
  • - +
  • @@ -29,5 +30,5 @@ export default function Nav() { } `} - ) + ); } diff --git a/examples/with-react-intl/lang/en.json b/examples/with-react-intl/lang/en.json index d6de3be22b..e6cd30bb2f 100644 --- a/examples/with-react-intl/lang/en.json +++ b/examples/with-react-intl/lang/en.json @@ -1,7 +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!" + "11754": "An example app integrating React Intl with Next.js", + "65a8e": "Hello, World!", + "8cf04": "Home", + "8f7f4": "About", + "9c817": "React Intl Next.js Example" } diff --git a/examples/with-react-intl/lang/fr.json b/examples/with-react-intl/lang/fr.json index 60e5ca6228..10c7641a61 100644 --- a/examples/with-react-intl/lang/fr.json +++ b/examples/with-react-intl/lang/fr.json @@ -1,7 +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!" + "11754": "Un exemple d'application intégrant React Intl avec Next.js", + "65a8e": "Bonjour le monde!", + "8cf04": "Accueil", + "8f7f4": "À propos de nous", + "9c817": "React Intl Next.js Exemple" } diff --git a/examples/with-react-intl/next-env.d.ts b/examples/with-react-intl/next-env.d.ts new file mode 100644 index 0000000000..7b7aa2c772 --- /dev/null +++ b/examples/with-react-intl/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/with-react-intl/next.config.js b/examples/with-react-intl/next.config.js deleted file mode 100644 index 53fbb721c7..0000000000 --- a/examples/with-react-intl/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const withTM = require('next-transpile-modules')([ - '@formatjs/intl-relativetimeformat', - '@formatjs/intl-utils', - 'react-intl', - 'intl-format-cache', - 'intl-messageformat-parser', - 'intl-messageformat', -]) - -module.exports = withTM() diff --git a/examples/with-react-intl/package.json b/examples/with-react-intl/package.json index c097fa35cd..73e90e87c2 100644 --- a/examples/with-react-intl/package.json +++ b/examples/with-react-intl/package.json @@ -2,28 +2,41 @@ "name": "with-react-intl", "version": "1.0.0", "scripts": { - "dev": "node --icu-data-dir=node_modules/full-icu server.js", - "build": "next build && npm run extract", - "extract": "node ./scripts/extract '{pages,components}/*.{js,ts,tsx}'", - "start": "NODE_ENV=production node --icu-data-dir=node_modules/full-icu server.js" + "dev": "NODE_ICU_DATA=node_modules/full-icu ts-node --project tsconfig.server.json server.ts", + "build": "npm run extract:i18n && npm run compile:i18n && next build && tsc -p tsconfig.server.json", + "extract:i18n": "formatjs extract '{pages,components}/*.{js,ts,tsx}' --format simple --out-file lang/en.json", + "compile:i18n": "formatjs compile-folder --ast --format simple lang/ compiled-lang/", + "start": "NODE_ENV=production NODE_ICU_DATA=node_modules/full-icu node dist/server" }, "dependencies": { - "@formatjs/cli": "1.1.12", - "@formatjs/intl-relativetimeformat": "^2.8.2", - "@formatjs/intl-utils": "^0.6.1", + "@formatjs/cli": "^2.7.3", + "@formatjs/intl-datetimeformat": "^2.4.3", + "@formatjs/intl-getcanonicallocales": "^1.3.2", + "@formatjs/intl-numberformat": "^5.4.1", + "@formatjs/intl-pluralrules": "^3.4.0", + "@formatjs/intl-relativetimeformat": "^7.1.1", "accepts": "^1.3.7", + "babel-plugin-react-intl": "^8.1.1", "full-icu": "^1.3.0", "glob": "^7.1.4", - "intl": "^1.2.5", - "intl-locales-supported": "1.8.4", "next": "latest", "react": "^16.9.0", "react-dom": "^16.9.0", - "react-intl": "^3.1.12" + "react-intl": "^5.6.3" }, "license": "ISC", "devDependencies": { + "@types/accepts": "^1.3.5", "cross-spawn": "7.0.3", - "next-transpile-modules": "^4.0.2" + "prettier": "2.0.5", + "ts-node": "8.0.0", + "typescript": "3.9.7" + }, + "prettier": { + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": false, + "endOfLine": "lf", + "arrowParens": "avoid" } } diff --git a/examples/with-react-intl/pages/_app.js b/examples/with-react-intl/pages/_app.js deleted file mode 100644 index 87acb14fac..0000000000 --- a/examples/with-react-intl/pages/_app.js +++ /dev/null @@ -1,36 +0,0 @@ -import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl' - -// This is optional but highly recommended -// since it prevents memory leak -const cache = createIntlCache() - -function MyApp({ Component, pageProps, locale, messages }) { - const intl = createIntl( - { - locale, - messages, - }, - cache - ) - return ( - - - - ) -} - -MyApp.getInitialProps = async ({ Component, ctx }) => { - let pageProps = {} - - const { req } = ctx - const locale = req?.locale ?? '' - const messages = req?.messages ?? {} - - if (Component.getInitialProps) { - Object.assign(pageProps, await Component.getInitialProps(ctx)) - } - - return { pageProps, locale, messages } -} - -export default MyApp diff --git a/examples/with-react-intl/pages/_app.tsx b/examples/with-react-intl/pages/_app.tsx new file mode 100644 index 0000000000..fcdd1a8ec1 --- /dev/null +++ b/examples/with-react-intl/pages/_app.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import {IntlProvider} from 'react-intl'; +import {polyfill} from '../polyfills'; +import App from 'next/app'; + +function MyApp({Component, pageProps, locale, messages}) { + return ( + + + + ); +} + +// 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: string = 'en') => { + switch (locale) { + default: + return import('../compiled-lang/en.json'); + case 'fr': + return import('../compiled-lang/fr.json'); + } +}; + +const getInitialProps: typeof App.getInitialProps = async appContext => { + const { + ctx: {req}, + } = appContext; + const locale = (req as any)?.locale ?? 'en'; + + const [appProps, messages] = await Promise.all([ + polyfill(locale), + getMessages(locale), + App.getInitialProps(appContext), + ]); + + return {...(appProps as any), locale, messages}; +}; + +MyApp.getInitialProps = getInitialProps; + +export default MyApp; diff --git a/examples/with-react-intl/pages/_document.js b/examples/with-react-intl/pages/_document.js deleted file mode 100644 index 13cbb5612f..0000000000 --- a/examples/with-react-intl/pages/_document.js +++ /dev/null @@ -1,38 +0,0 @@ -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: { locale, localeDataScript }, - } = context - return { - ...props, - locale, - localeDataScript, - } - } - - render() { - // Polyfill Intl API for older browsers - const polyfill = `https://cdn.polyfill.io/v3/polyfill.min.js?features=Intl.~locale.${this.props.locale}` - - return ( - - - -
    -