Frontend i18n Infrastructure (#174)

This commit is contained in:
AMStrix 2018-11-04 12:26:34 -06:00 committed by Daniel Ternyak
parent 118d7b645e
commit c3649d322b
11 changed files with 244 additions and 35 deletions

View File

@ -1,18 +1,16 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { withNamespaces, WithNamespaces } from 'react-i18next';
import HeaderDetails from 'components/HeaderDetails';
import Rocket from 'static/images/rocket.svg';
import './style.less';
export default class Home extends React.Component {
class Home extends React.Component<WithNamespaces> {
render() {
const { t } = this.props;
return (
<div className="Home">
<HeaderDetails
title="Home"
description="Grant.io organizes creators and community members to incentivize ecosystem
improvements"
/>
<HeaderDetails title={t('home.title')} description={t('home.description')} />
<div className="Home-hero">
<div className="Home-hero-background">
<div className="Home-hero-background-planets">
@ -24,15 +22,15 @@ export default class Home extends React.Component {
<div className="Home-hero-inner">
<h1 className="Home-hero-title">
Decentralized funding for <br /> Blockchain ecosystem improvements
{t('home.heroTitle1')} <br /> {t('home.heroTitle2')}
</h1>
<div className="Home-hero-buttons">
<Link className="Home-hero-buttons-button is-primary" to="/create">
Propose a Project
{t('home.createButton')}
</Link>
<Link className="Home-hero-buttons-button" to="/proposals">
Explore Projects
{t('home.exploreButton')}
</Link>
</div>
</div>
@ -41,3 +39,5 @@ export default class Home extends React.Component {
);
}
}
export default withNamespaces()(Home);

17
frontend/client/i18n.ts Normal file
View File

@ -0,0 +1,17 @@
import i18n from 'i18next';
// NOTE: maintain parity with server/i18n.ts
i18n.init({
whitelist: ['en'],
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
interpolation: {
// not needed for react
escapeValue: false,
},
});
export default i18n;

View File

@ -6,20 +6,27 @@ import { loadComponents } from 'loadable-components';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import { I18nextProvider } from 'react-i18next';
import { configureStore } from 'store/configure';
import Routes from './Routes';
import i18n from './i18n';
const initialState = window && (window as any).__PRELOADED_STATE__;
const { store, persistor } = configureStore(initialState);
const i18nLanguage = window && (window as any).__PRELOADED_I18N__;
i18n.changeLanguage(i18nLanguage.locale);
i18n.addResourceBundle(i18nLanguage.locale, 'common', i18nLanguage.resources, true);
const App = hot(module)(() => (
<Provider store={store}>
<PersistGate persistor={persistor}>
<Router>
<Routes />
</Router>
</PersistGate>
</Provider>
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<PersistGate persistor={persistor}>
<Router>
<Routes />
</Router>
</PersistGate>
</Provider>
</I18nextProvider>
));
loadComponents().then(() => {

View File

@ -0,0 +1,10 @@
{
"home": {
"title": "Home",
"description": "Grant.io organizes creators and community members to incentivize ecosystem improvements",
"heroTitle1": "Decentralized funding for",
"heroTitle2": "Blockchain ecosystem improvements",
"createButton": "Propose a Project",
"exploreButton": "Explore Projects"
}
}

View File

@ -5,6 +5,7 @@ const { StatsWriterPlugin } = require('webpack-stats-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ModuleDependencyWarning = require('./module-dependency-warning');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const env = require('../env')();
const paths = require('../paths');
@ -44,6 +45,15 @@ const client = [
theme_color: '#ffffff',
},
}),
new CopyWebpackPlugin([
{
from: 'client/static/locales/**/*.json',
transformPath(targetPath, absolutePath) {
const match = targetPath.match(/locales\/(.+)\/(.+\.json)$/);
return `locales/${match[1]}/${match[2]}`;
},
},
]),
// this allows the server access to the dependency graph
// so it can find which js/css to add to initial page
new StatsWriterPlugin({

View File

@ -46,6 +46,8 @@
"@types/dotenv": "^4.0.3",
"@types/express": "^4.16.0",
"@types/express-winston": "^3.0.0",
"@types/i18next-express-middleware": "^0.0.33",
"@types/i18next-node-fs-backend": "^0.0.30",
"@types/js-cookie": "2.1.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.112",
@ -77,6 +79,7 @@
"chalk": "^2.4.1",
"classnames": "^2.2.6",
"cookie-parser": "^1.4.3",
"copy-webpack-plugin": "^4.6.0",
"core-js": "^2.5.7",
"cors": "^2.8.4",
"cross-env": "^5.2.0",
@ -97,6 +100,9 @@
"http-proxy-middleware": "^0.18.0",
"https-proxy": "0.0.2",
"husky": "^1.0.0-rc.8",
"i18next": "^12.0.0",
"i18next-express-middleware": "^1.4.1",
"i18next-node-fs-backend": "^2.1.0",
"js-cookie": "^2.2.0",
"less": "^3.7.1",
"less-loader": "^4.1.0",
@ -115,6 +121,7 @@
"react-dom": "16.5.2",
"react-helmet": "^5.2.0",
"react-hot-loader": "^4.3.8",
"react-i18next": "^8.3.5",
"react-mde": "^5.8.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",

View File

@ -8,6 +8,7 @@ export interface Props {
linkTags: Array<React.LinkHTMLAttributes<HTMLLinkElement>>;
metaTags: Array<React.MetaHTMLAttributes<HTMLMetaElement>>;
state: string;
i18n: string;
loadableStateScript: string;
}
@ -16,6 +17,7 @@ const HTML: React.SFC<Props> = ({
scripts,
css,
state,
i18n,
linkTags,
metaTags,
loadableStateScript,
@ -63,6 +65,11 @@ const HTML: React.SFC<Props> = ({
__html: `window.__PRELOADED_STATE__ = ${state}`,
}}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.__PRELOADED_I18N__ = ${i18n}`,
}}
/>
</head>
<body>
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />

35
frontend/server/i18n.ts Normal file
View File

@ -0,0 +1,35 @@
import path from 'path';
import i18n from 'i18next';
import Backend from 'i18next-node-fs-backend';
import { LanguageDetector } from 'i18next-express-middleware';
// @ts-ignore
import * as paths from '../config/paths';
const publicPath = path.join(paths.clientBuild, paths.publicPath);
// NOTE: maintain parity with client/i18n.ts
i18n
.use(Backend)
.use(LanguageDetector)
.init({
whitelist: ['en'],
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
debug: false,
interpolation: {
// not needed for react
escapeValue: false,
},
backend: {
loadPath: publicPath + 'locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
},
});
export default i18n;

View File

@ -6,11 +6,13 @@ import manifestHelpers from 'express-manifest-helpers';
import * as bodyParser from 'body-parser';
import dotenv from 'dotenv';
import expressWinston from 'express-winston';
import i18nMiddleware from 'i18next-express-middleware';
import log from './log';
import serverRender from './render';
// @ts-ignore
import * as paths from '../config/paths';
import log from './log';
import serverRender from './render';
import i18n from './i18n';
process.env.SERVER_SIDE_RENDER = 'true';
const isDev = process.env.NODE_ENV === 'development';
@ -22,13 +24,15 @@ const app = express();
// log requests
app.use(expressWinston.logger({ winstonInstance: log }));
// i18next
app.use(i18nMiddleware.handle(i18n));
if (isDev) {
app.use(
paths.publicPath,
express.static(path.join(paths.clientBuild, paths.publicPath)),
);
// tslint:disable-next-line:variable-name
app.use('/favicon.ico', (_req, res) => {
app.use('/favicon.ico', (_, res) => {
res.send('');
});
} else {
@ -37,8 +41,7 @@ if (isDev) {
paths.publicPath,
express.static(path.join(paths.clientBuild, paths.publicPath)),
);
// tslint:disable-next-line:variable-name
app.use('/favicon.ico', (_req, res) => {
app.use('/favicon.ico', (_, res) => {
res.send('');
});
}

View File

@ -1,9 +1,12 @@
import fs from 'fs';
import path from 'path';
import React from 'react';
import { Request, Response } from 'express';
import { renderToString } from 'react-dom/server';
import { getLoadableState } from 'loadable-components/server';
import { StaticRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import log from './log';
import { configureStore } from '../client/store/configure';
@ -11,9 +14,8 @@ import Html from './components/HTML';
import Routes from '../client/Routes';
import linkTags from './linkTags';
import metaTags from './metaTags';
import i18n from './i18n';
import fs from 'fs';
import path from 'path';
// @ts-ignore
import * as paths from '../config/paths';
const isDev = process.env.NODE_ENV === 'development';
@ -76,12 +78,22 @@ const chunkExtractFromLoadables = (loadableState: any) =>
const serverRenderer = () => async (req: Request, res: Response) => {
const { store } = configureStore();
// i18n
const locale = (req as any).language;
const resources = i18n.getResourceBundle(locale, 'common');
const i18nClient = JSON.stringify({ locale, resources });
const i18nServer = i18n.cloneInstance();
i18nServer.changeLanguage(locale);
const reactApp = (
<Provider store={store}>
<Router location={req.url} context={{}}>
<Routes />
</Router>
</Provider>
<I18nextProvider i18n={i18nServer}>
<Provider store={store}>
<Router location={req.url} context={{}}>
<Routes />
</Router>
</Provider>
</I18nextProvider>
);
let loadableState;
@ -132,6 +144,7 @@ const serverRenderer = () => async (req: Request, res: Response) => {
linkTags={mappedLinkTags}
metaTags={mappedMetaTags}
state={state}
i18n={i18nClient}
loadableStateScript={loadableState.getScriptContent()}
>
{content}

View File

@ -1151,6 +1151,12 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
dependencies:
regenerator-runtime "^0.12.0"
"@babel/template@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0.tgz#c2bc9870405959c89a9c814376a2ecb247838c80"
@ -1902,6 +1908,27 @@
version "4.7.0"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.0.tgz#2fac51050c68f7d6f96c5aafc631132522f4aa3f"
"@types/i18next-express-middleware@^0.0.33":
version "0.0.33"
resolved "http://registry.npmjs.org/@types/i18next-express-middleware/-/i18next-express-middleware-0.0.33.tgz#1c5625f123eaae126de3b43626ef9a04bc6ad482"
dependencies:
"@types/express" "*"
"@types/i18next" "*"
"@types/i18next-node-fs-backend@^0.0.30":
version "0.0.30"
resolved "https://registry.yarnpkg.com/@types/i18next-node-fs-backend/-/i18next-node-fs-backend-0.0.30.tgz#7454f8e923798a2ebf16309befcfdfde32e90a7c"
dependencies:
"@types/i18next" "^2"
"@types/i18next@*":
version "11.9.3"
resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-11.9.3.tgz#04d84c6539908ad69665d26d8967f942d1638550"
"@types/i18next@^2":
version "2.3.41"
resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-2.3.41.tgz#5a3ebdcb4942052ca2ef71c4f6341438c57cb18c"
"@types/js-cookie@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.1.0.tgz#a8916246aa994db646c66d54c854916213300a51"
@ -4575,6 +4602,13 @@ cookiejar@^2.1.0, cookiejar@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
cookies@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b"
dependencies:
depd "~1.1.1"
keygrip "~1.0.2"
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@ -4613,6 +4647,19 @@ copy-webpack-plugin@^4.2.0:
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
copy-webpack-plugin@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
dependencies:
cacache "^10.0.4"
find-cache-dir "^1.0.0"
globby "^7.1.1"
is-glob "^4.0.0"
loader-utils "^1.1.0"
minimatch "^3.0.4"
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -4711,6 +4758,13 @@ create-react-context@0.2.2:
fbjs "^0.8.0"
gud "^1.0.0"
create-react-context@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
dependencies:
fbjs "^0.8.0"
gud "^1.0.0"
cross-env@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
@ -7453,16 +7507,16 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
hoist-non-react-statics@^3.0.1:
hoist-non-react-statics@3.0.1, hoist-non-react-statics@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364"
dependencies:
react-is "^16.3.2"
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -7535,6 +7589,12 @@ html-minifier@^3.5.9:
relateurl "0.2.x"
uglify-js "3.4.x"
html-parse-stringify2@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
dependencies:
void-elements "^2.0.1"
htmlparser2@^3.9.1:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@ -7655,6 +7715,23 @@ hyphenate-style-name@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
i18next-express-middleware@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-1.4.1.tgz#273c4a490ad688ce246815ce1288689c63fa7de1"
dependencies:
cookies "0.7.1"
i18next-node-fs-backend@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/i18next-node-fs-backend/-/i18next-node-fs-backend-2.1.0.tgz#b0ad55eb8671b4dedbd21fe434fb50e964a4ece2"
dependencies:
js-yaml "3.12.0"
json5 "2.0.0"
i18next@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-12.0.0.tgz#27c1494219dde0451a8d714d5bfc19bf055d51bb"
iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.4:
version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
@ -8800,7 +8877,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0:
js-yaml@3.12.0, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
dependencies:
@ -8920,6 +8997,12 @@ json3@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
json5@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.0.0.tgz#b61abf97aa178c4b5853a66cc8eecafd03045d78"
dependencies:
minimist "^1.2.0"
json5@2.x, json5@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.0.1.tgz#3d6d0d1066039eb50984e66a7840e4f4b7a2c660"
@ -9004,6 +9087,10 @@ keycode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
keygrip@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
killable@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@ -12547,6 +12634,15 @@ react-hot-loader@^4.3.8:
react-lifecycles-compat "^3.0.4"
shallowequal "^1.0.2"
react-i18next@^8.3.5:
version "8.3.5"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-8.3.5.tgz#7a24f2715646bf8027cc39aca04c94e8c0b130c0"
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "0.2.3"
hoist-non-react-statics "3.0.1"
html-parse-stringify2 "2.0.1"
react-inspector@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-2.3.0.tgz#fc9c1d38ab687fc0d190dcaf133ae40158968fc8"
@ -15595,6 +15691,10 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
void-elements@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
w3c-hr-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"