feat: upgrade react-intl workflow in example (#16215)
Changes: - Migrate to TypeScript. `react-intl` natively supports TypeScript now. - Upgrade corresponding `formatjs` packages. - Dynamically polyfill Intl API per locale since those polyfills are huge. - Migrate to recommended workflow per https://formatjs.io/docs/getting-started/application-workflow
This commit is contained in:
parent
f658b7641d
commit
5a478b4eef
25 changed files with 362 additions and 352 deletions
10
examples/with-react-intl/.babelrc
Normal file
10
examples/with-react-intl/.babelrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [
|
||||
["babel-plugin-react-intl", {
|
||||
"ast": true,
|
||||
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
|
||||
"extractFromFormatMessageCall": true
|
||||
}]
|
||||
]
|
||||
}
|
1
examples/with-react-intl/.gitignore
vendored
1
examples/with-react-intl/.gitignore
vendored
|
@ -33,3 +33,4 @@ yarn-error.log*
|
|||
|
||||
# vercel
|
||||
.vercel
|
||||
compiled-lang
|
3
examples/with-react-intl/.vscode/settings.json
vendored
Normal file
3
examples/with-react-intl/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
|
@ -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
|
||||
- `<IntlProvider>` 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 `<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:
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<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/Layout.tsx
Normal file
28
examples/with-react-intl/components/Layout.tsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>
|
||||
{title ||
|
||||
intl.formatMessage({
|
||||
defaultMessage: 'React Intl Next.js Example',
|
||||
})}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
<header>
|
||||
<Nav />
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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() {
|
|||
<li>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<FormattedMessage id="nav.home" defaultMessage="Home" />
|
||||
<FormattedMessage defaultMessage="Home" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about">
|
||||
<a>
|
||||
<FormattedMessage id="nav.about" defaultMessage="About" />
|
||||
<FormattedMessage defaultMessage="About" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -29,5 +30,5 @@ export default function Nav() {
|
|||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
2
examples/with-react-intl/next-env.d.ts
vendored
Normal file
2
examples/with-react-intl/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
|
@ -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()
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<RawIntlProvider value={intl}>
|
||||
<Component {...pageProps} />
|
||||
</RawIntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
43
examples/with-react-intl/pages/_app.tsx
Normal file
43
examples/with-react-intl/pages/_app.tsx
Normal file
|
@ -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 (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Component {...pageProps} />
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
|
@ -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 (
|
||||
<html>
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<script src={polyfill} />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.localeDataScript,
|
||||
}}
|
||||
/>
|
||||
<NextScript />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { FormattedRelativeTime } from 'react-intl'
|
||||
import { selectUnit } from '@formatjs/intl-utils'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
export default function About() {
|
||||
const { value, unit } = selectUnit(Date.now())
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
<FormattedRelativeTime
|
||||
numeric="auto"
|
||||
value={value}
|
||||
unit={unit}
|
||||
updateIntervalInSeconds={1}
|
||||
/>
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
14
examples/with-react-intl/pages/about.tsx
Normal file
14
examples/with-react-intl/pages/about.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as React from 'react';
|
||||
import {FormattedRelativeTime, useIntl} from 'react-intl';
|
||||
import Layout from '../components/Layout';
|
||||
|
||||
export default function About() {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Layout title={intl.formatMessage({defaultMessage: 'About'})}>
|
||||
<p>
|
||||
<FormattedRelativeTime numeric="auto" value={1} unit="hour" />
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import {
|
||||
FormattedMessage,
|
||||
FormattedNumber,
|
||||
defineMessages,
|
||||
useIntl,
|
||||
} from 'react-intl'
|
||||
import Head from 'next/head'
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
const { description } = defineMessages({
|
||||
description: {
|
||||
id: 'description',
|
||||
defaultMessage: 'An example app integrating React Intl with Next.js',
|
||||
},
|
||||
})
|
||||
|
||||
export default function Home() {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<meta name="description" content={intl.formatMessage(description)} />
|
||||
</Head>
|
||||
<p>
|
||||
<FormattedMessage id="greeting" defaultMessage="Hello, World!" />
|
||||
</p>
|
||||
<p>
|
||||
<FormattedNumber value={1000} />
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
32
examples/with-react-intl/pages/index.tsx
Normal file
32
examples/with-react-intl/pages/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl';
|
||||
import Head from 'next/head';
|
||||
import Layout from '../components/Layout';
|
||||
|
||||
export default function Home() {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: 'Home',
|
||||
})}
|
||||
>
|
||||
<Head>
|
||||
<meta
|
||||
name="description"
|
||||
content={intl.formatMessage({
|
||||
defaultMessage:
|
||||
'An example app integrating React Intl with Next.js',
|
||||
})}
|
||||
/>
|
||||
</Head>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Hello, World!" />
|
||||
</p>
|
||||
<p>
|
||||
<FormattedNumber value={1000} />
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
127
examples/with-react-intl/polyfills.ts
Normal file
127
examples/with-react-intl/polyfills.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import {shouldPolyfill as shouldPolyfillGetCanonicalLocales} from '@formatjs/intl-getcanonicallocales/should-polyfill';
|
||||
import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-pluralrules/should-polyfill';
|
||||
import {shouldPolyfill as shouldPolyfillNumberFormat} from '@formatjs/intl-numberformat/should-polyfill';
|
||||
import {shouldPolyfill as shouldPolyfillDateTimeFormat} from '@formatjs/intl-datetimeformat/should-polyfill';
|
||||
import {shouldPolyfill as shouldPolyfillRelativeTimeFormat} from '@formatjs/intl-relativetimeformat/should-polyfill';
|
||||
|
||||
/**
|
||||
* Dynamically polyfill Intl API & its locale data
|
||||
* @param locale locale to polyfill
|
||||
*/
|
||||
export async function polyfill(locale: string = '') {
|
||||
const dataPolyfills = [];
|
||||
// Polyfill Intl.getCanonicalLocales if necessary
|
||||
if (shouldPolyfillGetCanonicalLocales()) {
|
||||
await import(
|
||||
/* webpackChunkName: "intl-getcanonicallocales" */ '@formatjs/intl-getcanonicallocales/polyfill'
|
||||
);
|
||||
}
|
||||
|
||||
// Polyfill Intl.PluralRules if necessary
|
||||
if (shouldPolyfillPluralRules()) {
|
||||
await import(
|
||||
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/polyfill'
|
||||
);
|
||||
}
|
||||
|
||||
if ((Intl.PluralRules as any).polyfilled) {
|
||||
const lang = locale.split('-')[0];
|
||||
switch (lang) {
|
||||
default:
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/locale-data/en'
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'fr':
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/locale-data/fr'
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill Intl.NumberFormat if necessary
|
||||
if (shouldPolyfillNumberFormat()) {
|
||||
await import(
|
||||
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/polyfill'
|
||||
);
|
||||
}
|
||||
|
||||
if ((Intl.NumberFormat as any).polyfilled) {
|
||||
switch (locale) {
|
||||
default:
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/locale-data/en'
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'fr':
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/locale-data/fr'
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill Intl.DateTimeFormat if necessary
|
||||
if (shouldPolyfillDateTimeFormat()) {
|
||||
await import(
|
||||
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/polyfill'
|
||||
);
|
||||
}
|
||||
|
||||
if ((Intl.DateTimeFormat as any).polyfilled) {
|
||||
dataPolyfills.push(import('@formatjs/intl-datetimeformat/add-all-tz'));
|
||||
switch (locale) {
|
||||
default:
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/locale-data/en'
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'fr':
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/locale-data/fr'
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill Intl.RelativeTimeFormat if necessary
|
||||
if (shouldPolyfillRelativeTimeFormat()) {
|
||||
await import(
|
||||
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/polyfill'
|
||||
);
|
||||
}
|
||||
|
||||
if ((Intl as any).RelativeTimeFormat.polyfilled) {
|
||||
switch (locale) {
|
||||
default:
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/locale-data/en'
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'fr':
|
||||
dataPolyfills.push(
|
||||
import(
|
||||
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/locale-data/fr'
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(dataPolyfills);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
const { readFileSync, writeFileSync } = require('fs')
|
||||
const { resolve } = require('path')
|
||||
const glob = require('glob')
|
||||
const spawn = require('cross-spawn')
|
||||
|
||||
// formatjs cli doesn't currently support globbing, so we perform it ourselves
|
||||
// as a workaround. see https://github.com/formatjs/formatjs/issues/383
|
||||
const sourceFiles = glob.sync(process.argv[2])
|
||||
spawn.sync('npx', [
|
||||
'formatjs',
|
||||
'extract',
|
||||
'--messages-dir',
|
||||
'lang/.messages/',
|
||||
'--remove-default-message',
|
||||
...sourceFiles,
|
||||
])
|
||||
|
||||
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) && messages[id] !== defaultMessage) {
|
||||
throw new Error(
|
||||
`Duplicate message id: ${id} (duplicate message ids are allowed, but only if the defaultMessages match!)`
|
||||
)
|
||||
}
|
||||
messages[id] = defaultMessage
|
||||
})
|
||||
return messages
|
||||
}, {})
|
||||
|
||||
writeFileSync('./lang/en.json', JSON.stringify(defaultMessages, null, 2))
|
||||
console.log(`> Wrote default messages to: "${resolve('./lang/en.json')}"`)
|
|
@ -1,83 +0,0 @@
|
|||
const { basename } = require('path')
|
||||
const glob = require('glob')
|
||||
const areIntlLocalesSupported = require('intl-locales-supported').default
|
||||
|
||||
// Get the supported languages by looking for translations in the `lang/` dir.
|
||||
const supportedLanguages = glob
|
||||
.sync('./lang/*.json')
|
||||
.map((f) => basename(f, '.json'))
|
||||
|
||||
// Polyfill Node with `Intl` that has data for all locales.
|
||||
// See: https://formatjs.io/guides/runtime-environments/#server
|
||||
if (global.Intl) {
|
||||
// Determine if the built-in `Intl` has the locale data we need.
|
||||
if (!areIntlLocalesSupported(supportedLanguages)) {
|
||||
// `Intl` exists, but it doesn't have the data we need, so load the
|
||||
// polyfill and patch the constructors we need with the polyfills.
|
||||
const IntlPolyfill = require('intl')
|
||||
Intl.NumberFormat = IntlPolyfill.NumberFormat
|
||||
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat
|
||||
Intl.__disableRegExpRestore = IntlPolyfill.__disableRegExpRestore
|
||||
}
|
||||
} else {
|
||||
// No `Intl`, so use and load the polyfill.
|
||||
global.Intl = require('intl')
|
||||
}
|
||||
|
||||
// Fix: https://github.com/vercel/next.js/issues/11777
|
||||
// See related issue: https://github.com/andyearnshaw/Intl.js/issues/308
|
||||
if (Intl.__disableRegExpRestore) {
|
||||
Intl.__disableRegExpRestore()
|
||||
}
|
||||
|
||||
// Polyfill DOMParser for **pre-v4** react-intl used by formatjs.
|
||||
// Only needed when using FormattedHTMLMessage. Make sure to install `xmldom`.
|
||||
// See: https://github.com/vercel/next.js/issues/10533
|
||||
// const { DOMParser } = require('xmldom')
|
||||
// global.DOMParser = DOMParser
|
||||
|
||||
const { readFileSync } = require('fs')
|
||||
const { createServer } = require('http')
|
||||
const accepts = require('accepts')
|
||||
const next = require('next')
|
||||
|
||||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
// 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(
|
||||
`@formatjs/intl-relativetimeformat/dist/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(supportedLanguages) || 'en'
|
||||
req.locale = locale
|
||||
req.localeDataScript = getLocaleDataScript(locale)
|
||||
req.messages = dev ? {} : getMessages(locale)
|
||||
handle(req, res)
|
||||
}).listen(port, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`> Ready on http://localhost:${port}`)
|
||||
})
|
||||
})
|
29
examples/with-react-intl/server.ts
Normal file
29
examples/with-react-intl/server.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {basename} from 'path';
|
||||
import {sync as globSync} from 'glob';
|
||||
import {createServer} from 'http';
|
||||
import accepts from 'accepts';
|
||||
import next from 'next';
|
||||
import {polyfill} from './polyfills';
|
||||
// Get the supported languages by looking for translations in the `lang/` dir.
|
||||
const supportedLanguages = globSync('./compiled-lang/*.json').map(f =>
|
||||
basename(f, '.json')
|
||||
);
|
||||
|
||||
const SUPPORTED_LOCALES = ['en', 'fr'];
|
||||
|
||||
const port = parseInt(process.env.PORT, 10) || 3000;
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const app = next({dev});
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
Promise.all([app.prepare(), ...SUPPORTED_LOCALES.map(polyfill)]).then(() => {
|
||||
createServer((req, res) => {
|
||||
const accept = accepts(req);
|
||||
const locale = accept.language(supportedLanguages) || 'en';
|
||||
(req as any).locale = locale;
|
||||
|
||||
handle(req, res);
|
||||
}).listen(port, () => {
|
||||
console.log(`> Ready on http://localhost:${port}`);
|
||||
});
|
||||
});
|
19
examples/with-react-intl/tsconfig.json
Normal file
19
examples/with-react-intl/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext", "ESNext.Intl", "ES2018.Intl"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
8
examples/with-react-intl/tsconfig.server.json
Normal file
8
examples/with-react-intl/tsconfig.server.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "CommonJS",
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue