Hi there,
i try to use react-i18next with next-js. I followed the example provide by next-js repository (https://github.com/zeit/next.js/tree/canary/examples/with-react-i18next).
Everythings seems to work but when i start my project, i need to reload (F5) my page to see the translation.
Maybe it's an SSR issue, but i'm a newbie and i can't figure out this...
Thanks for help! :-)
Below my files:
i18n.js
const i18next = require('i18next');
const XHR = require('i18next-xhr-backend');
const LanguageDetector = require('i18next-browser-languagedetector');
const options = {
fallbackLng: 'fr',
load: 'languageOnly', // We only provide en -> no region specific locals like en-US, de-DE.
// Have a common namespace used around the full app.
defaultNS: 'common',
debug: process.env.NODE_ENV !== 'production',
saveMissing: false,
interpolation: {
escapeValue: false, // Not needed for React !!
formatSeparator: ',',
format: (value, format, lng) => {
if (format === 'uppercase') return value.toUpperCase();
return value;
}
}
};
const i18nInstance = i18next;
// For browser use xhr backend to load translations and browser lng detector.
if (process.browser) {
i18nInstance
.use(XHR)
// .use(Cache)
.use(LanguageDetector);
}
// Initialize if not already initialized.
if (!i18nInstance.isInitialized) i18nInstance.init(options);
// A simple helper to getInitialProps passed on loaded i18n data.
const getInitialProps = (req, namespaces) => {
if (!namespaces) namespaces = i18nInstance.options.defaultNS; // eslint-disable-line no-param-reassign
if (typeof namespaces === 'string') namespaces = [namespaces]; // eslint-disable-line no-param-reassign
req.i18n.toJSON = () => null; // Do not serialize i18next instance and send to client.
const initialI18nStore = {};
req.i18n.languages.forEach(l => {
initialI18nStore[l] = {};
namespaces.forEach(ns => {
initialI18nStore[l][ns] =
(req.i18n.services.resourceStore.data[l] || {})[ns] || {};
});
});
return {
i18n: req.i18n, // Use the instance on req - fixed language on request (avoid issues in race conditions with lngs of different users).
initialI18nStore,
initialLanguage: req.i18n.language
};
};
module.exports = {
getInitialProps,
i18nInstance,
I18n: i18next.default
};
index.js
require('./global_functions');
const next = require('next');
const path = require('path');
const fs = require('fs');
const compression = require('compression');
const nextAuth = require('next-auth');
const isDev = process.env.NODE_ENV !== 'production';
const isProd = !isDev;
const ngrok = isDev ? require('ngrok') : null;
// Load environment variables from .env file if present ini dev mode
if (isDev) require('dotenv').config();
const models = path.join(__dirname, 'api/models');
const nextAuthConfig = require('./next-auth.config');
const logger = require('./server/logger');
const customHost = process.env.HOST;
const host = customHost || null;
const prettyHost = customHost || 'localhost';
const port = parseInt(process.env.PORT, 10) || 3000;
const routes = {
admin: require('./routes/admin'),
account: require('./routes/account'),
event: require('./routes/event'),
challenge: require('./routes/challenge'),
registration: require('./routes/registration'),
contact: require('./routes/contact')
};
// Bootstrap models
fs.readdirSync(models)
.filter(file => ~file.search(/^[^\.].*\.js$/))
.forEach(file => require(path.join(models, file)));
// Initialize Next.js
const nextApp = next({
dir: '.',
dev: process.env.NODE_ENV === 'development'
});
const i18nextMiddleware = require('i18next-express-middleware');
const Backend = require('i18next-node-fs-backend');
const { i18nInstance } = require('./i18n');
// Init i18next with serverside settings.
// Using i18next-express-middleware.
i18nInstance
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(
{
whitelist: ['fr', 'en'],
fallbackLng: 'fr',
preload: ['fr', 'en'], // Preload all langages.
defaultNS: 'common',
ns: [
'common',
'about',
'team',
'products',
'csr',
'ambassador',
'contact',
'press',
'hotline',
'portfolio',
'partners',
'home',
'privacy'
], // Need to preload all the namespaces.
backend: {
loadPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.json'),
addPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.missing.json')
},
saveMissing: false,
react: {
wait: true
}
},
() => {
// Add next-auth to next app.
nextApp
.prepare()
.then(() =>
// Load configuration and return config object.
nextAuthConfig()
)
.then(nextAuthOptions => nextAuth(nextApp, nextAuthOptions))
.then(nextAuthOptions => {
const { express, expressApp } = nextAuthOptions;
// Enable middleware for i18next.
expressApp.use(i18nextMiddleware.handle(i18nInstance));
// Serve locales for client.
expressApp.use(
'./locales',
express.static(path.join(__dirname, '/locales'))
);
// Missing keys.
expressApp.post(
'./locales/add/:lng/:ns',
i18nextMiddleware.missingKeyHandler(i18nInstance)
);
// Add admin routes.
routes.admin(expressApp);
// Add account management route - reuses functions defined for NextAuth.
routes.account(expressApp, nextAuthOptions.functions);
// Other API routes.
routes.event(expressApp);
routes.challenge(expressApp);
routes.registration(expressApp);
routes.contact(expressApp);
// GZIP enabled ini production.
expressApp.use(compression());
// Serve fonts from ionicon npm module.
expressApp.use(
'/fonts/ionicons',
express.static('./node_modules/ionicons/dist/fonts')
);
if (isProd) {
expressApp.get('/service-worker.js', (req, res) => {
const filePath = path.join(
__dirname,
'.next',
'/service-worker.js'
);
nextApp.serveStatic(req, res, path.resolve(filePath));
});
expressApp.get('/sitemap.xml', (req, res) =>
nextApp.serveStatic(
req,
res,
path.resolve('./static/sitemap.xml')
)
);
expressApp.get('/robots.txt', (req, res) =>
nextApp.serveStatic(req, res, path.resolve('./static/robots.txt'))
);
}
// Default catch-all handler to allow Next.js to handle all other routes.
expressApp.all('*', (req, res) => {
const nextRequestHandler = nextApp.getRequestHandler();
return nextRequestHandler(req, res);
});
expressApp.listen(port, host, err => {
if (err) {
return logger.error(err.message);
}
if (ngrok) {
ngrok
.connect({ addr: port, region: 'eu' })
.then(url => logger.appStarted(port, prettyHost, url))
.catch(e => logger.error(e));
} else {
console.log('Teewii application started');
}
});
})
.catch(err => {
console.log('An error occurred, unable to start the server');
console.log(err);
});
}
);
hoc/withI18next.js
import { translate, loadNamespaces } from 'react-i18next';
import { getInitialProps, I18n } from '../i18n';
const withI18next = (
namespaces = [
'common',
'about',
'team',
'products',
'csr',
'ambassador',
'contact',
'press',
'hotline',
'portfolio',
'partners',
'home',
'privacy'
]
) => ComposedComponent => {
const Extended = translate(namespaces, { i18n: I18n, wait: process.browser })(
ComposedComponent
);
Extended.getInitialProps = async ctx => {
const composedInitialProps = ComposedComponent.getInitialProps
? await ComposedComponent.getInitialProps(ctx)
: {};
const i18nInitialProps = ctx.req
? getInitialProps(ctx.req, namespaces)
: await loadNamespaces({
components: [{ props: { namespaces } }],
i18n: I18n
});
return {
...composedInitialProps,
...i18nInitialProps
};
};
return Extended;
};
export default withI18next;
If i try to render 'about' page, i have to reload..
pages/about.js
import React from 'react';
import Layout from '../container/layout';
import About from '../views/About';
import PublicNavbar from '../components/navbar';
import PublicFooter from '../components/footer';
import StarsBg from '../components/StarsBg';
import withI18next from '../hoc/withI18next';
export default withI18next(['about', 'common'])(({ t, initialI18nStore }) => (
<Layout
title="title"
description="description"
>
<StarsBg />
<div style={{ position: 'relative', zIndex: '1' }}>
<PublicNavbar />
<About t={t} />
<PublicFooter />
</div>
</Layout>
));
views/About/index.js
/* eslint-disable react/no-danger */
import React from 'react';
import Link from 'next/link';
import colors from '../../utils/colors';
import FitText from '../../utils/fitText';
export default ({ t }) => (
<div className="content">
<FitText maxFontSize={72} minFontSize={35}>
<h1>{t('whoWeAre-title')}</h1>
</FitText>
<div className="bordered-text">
<p dangerouslySetInnerHTML={{ __html: t('whoWeAre-p1') }} />
<p dangerouslySetInnerHTML={{ __html: t('whoWeAre-p2') }} />
<p dangerouslySetInnerHTML={{ __html: t('whoWeAre-p3') }} />
<p dangerouslySetInnerHTML={{ __html: t('whoWeAre-p4') }} />
<p>{t('whoWeAre-team')}</p>
</div>
<Link href="/team">
<a>{t('whoWeAre-discover')}</a>
</Link>
</div>
......
);
hm...do you really expect me to put all those code snipplets into my brain and use it to run it like a browser to see what happens - just joking - but guess i can't help a lot by just looking at this code.
Try to check if translations get loaded correctly...if the sample provided works you might need to check what makes it break...
Yes you are right! I come back later with more investigation, thanks for the quick reply
I made a mistake and i just had to change this lines in my index.js:
// Serve locales for client.
expressApp.use(
'/locales', // instead of './locales'
express.static(path.join(__dirname, '/locales'))
);
// Missing keys.
expressApp.post(
'/locales/add/:lng/:ns', instead of './locales/add/:lng/:ns'
i18nextMiddleware.missingKeyHandler(i18nInstance)
);
Work like a charm now !
glad you figured it out.
Most helpful comment
hm...do you really expect me to put all those code snipplets into my brain and use it to run it like a browser to see what happens - just joking - but guess i can't help a lot by just looking at this code.
Try to check if translations get loaded correctly...if the sample provided works you might need to check what makes it break...