Skip to content

Instantly share code, notes, and snippets.

@revskill10
Last active August 1, 2019 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save revskill10/0f84ff38411a6254efb09be998572798 to your computer and use it in GitHub Desktop.
Save revskill10/0f84ff38411a6254efb09be998572798 to your computer and use it in GitHub Desktop.
React-i18next
/*
This `_app.js` component is a utility provided NextJS,
and will wrap all page-level components (normally
found within your `pages` directory). Crucially, this
component renders both on the server and on the client.
It serves to main purposes:
1. Set up initial translation on the server, and
serialize it to the client
2. Wrap all pages with a I18nextProvider
It's also worth noting that this is where nsFromReactTree
comes in, allowing you to just use the HOC without
worrying about specific React tree translation deps.
*/
import React from 'react';
import Router from 'next/router';
import { I18nextProvider } from 'react-i18next';
import NextApp, { Container } from 'next/app';
import {registerI18n, i18n} from 'i18n-client'
registerI18n(Router)
export default class App extends NextApp {
/*
The `getInitialProps` method is a built-in from
NextJs itself, and allows us to access the req
object from Express, and perform some setup
before actually rendering our React tree.
*/
static async getInitialProps({ Component, ctx }) {
// Recompile pre-existing pageProps
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
// Initiate vars to return
const { req } = ctx;
let initialI18nStore = {};
let initialLanguage = null;
// Load translations to serialize if we're serverside
if (req && req.i18n) {
[initialLanguage] = req.i18n.languages;
i18n.language = initialLanguage;
req.i18n.languages.forEach(l => {
initialI18nStore[l] = {};
i18n.nsFromReactTree.forEach(ns => {
initialI18nStore[l][ns] = (req.i18n.services.resourceStore.data[l] || {})[ns] || {};
});
});
} else {
// Load newly-required translations if changing route clientside
await Promise.all(
i18n.nsFromReactTree
.filter(ns => !i18n.hasResourceBundle(i18n.languages[0], ns))
.map(ns => new Promise(resolve => i18n.loadNamespaces(ns, () => resolve())))
);
initialI18nStore = i18n.store.data;
initialLanguage = i18n.language;
}
// `pageProps` will get serialized automatically by NextJs
return {
pageProps: {
initialI18nStore,
initialLanguage,
...pageProps,
},
};
}
render() {
const { Component, pageProps } = this.props;
let { initialLanguage, initialI18nStore } = pageProps;
if (!process.browser) {
initialLanguage = i18n.language;
initialI18nStore = i18n.store.data;
}
return (
<Container>
<I18nextProvider
i18n={i18n}
initialLanguage={initialLanguage}
initialI18nStore={initialI18nStore}
>
<Component {...pageProps} />
</I18nextProvider>
</Container>
);
}
}
function lngPathCorrection(config, i18n) {
const { defaultLanguage, allLanguages } = config.translation;
return function(currentRoute, currentLanguage = i18n.languages[0]) {
if (!allLanguages.includes(currentLanguage)) {
return currentRoute;
}
let href = currentRoute;
let as = href;
for (const lng of allLanguages) {
if (href.startsWith(`/${lng}/`)) {
href = href.replace(`/${lng}/`, '/');
break;
}
}
if (currentLanguage !== defaultLanguage) {
as = `/${currentLanguage}${href}`;
href += `?lng=${currentLanguage}`;
} else {
as = href;
}
return [href, as];
}
}
function makeConfig() {
const DEFAULT_LANGUAGE = 'en';
const OTHER_LANGUAGES = ['vi'];
const DEFAULT_NAMESPACE = 'common';
const LOCALE_PATH = 'static/locales';
const LOCALE_STRUCTURE = '{{lng}}/{{ns}}';
const LOCALE_SUBPATHS = false;
/* Core Settings - only change if you understand what you're changing */
const config = {
translation: {
allLanguages: OTHER_LANGUAGES.concat([DEFAULT_LANGUAGE]),
defaultLanguage: DEFAULT_LANGUAGE,
fallbackLng: DEFAULT_LANGUAGE,
load: 'languageOnly',
localesPath: `./${LOCALE_PATH}/`,
localeSubpaths: LOCALE_SUBPATHS,
ns: [DEFAULT_NAMESPACE],
defaultNS: DEFAULT_NAMESPACE,
interpolation: {
escapeValue: false,
formatSeparator: ',',
format: (value, format) => (format === 'uppercase' ? value.toUpperCase() : value),
},
detection: {
order: ['cookie', 'header', 'querystring'],
caches: ['cookie'],
},
backend: {
loadPath: `/${LOCALE_PATH}/${LOCALE_STRUCTURE}.json`,
addPath: `/${LOCALE_PATH}/${LOCALE_STRUCTURE}.missing.json`,
},
},
};
return config
}
function getI18n(config) {
const i18next = require('i18next');
const i18nextXHRBackend = require('i18next-xhr-backend');
const i18nextBrowserLanguageDetector = require('i18next-browser-languagedetector');
const i18n = i18next.default ? i18next.default : i18next;
i18n.nsFromReactTree = [];
i18n.use(i18nextXHRBackend).use(i18nextBrowserLanguageDetector);
if (!i18n.isInitialized) {
i18n.init(config.translation);
}
return i18n
}
function registerI18n(i18n, config) {
return function register(Router) {
if (config.translation.localeSubpaths) {
i18n.on('languageChanged', lng => {
if (process.browser) {
const originalRoute = window.location.pathname;
const [href, as] = lngPathCorrection(config, i18n)(originalRoute, lng);
if (as !== originalRoute) {
Router.replace(href, as, { shallow: true });
}
}
});
}
}
}
function withNs(namespaces=[]) {
const { withNamespaces } = require('react-i18next')
i18n.nsFromReactTree = [...new Set(i18n.nsFromReactTree.concat(namespaces))];
return withNamespaces(namespaces);
}
const config = makeConfig()
const i18n = getI18n(config)
module.exports = {
config,
i18n,
registerI18n: registerI18n(i18n, config),
withNamespaces: withNs,
}
function lngPathDetector(config) {
return function(req, res, cb) {
const { allLanguages, defaultLanguage } = config.translation;
if (req.i18n) {
const language = req.i18n.languages[0];
/*
If a user has hit a subpath which does not
match their language, give preference to
the path, and change user language.
*/
allLanguages.forEach(lng => {
if (req.url.startsWith(`/${lng}/`) && language !== lng) {
req.i18n.changeLanguage(lng);
}
});
/*
If a user has hit the root path and their
language is not set to default, give
preference to the path and reset their
language.
*/
if (language !== defaultLanguage && !req.url.startsWith(`/${language}/`)) {
req.i18n.changeLanguage(defaultLanguage);
}
/*
If a user has a default language prefix
in their URL, strip it.
*/
if (language === defaultLanguage && req.url.startsWith(`/${defaultLanguage}/`)) {
res.redirect(301, req.url.replace(`/${defaultLanguage}/`, '/'));
}
}
cb();
};
}
function forceTrailingSlash(config) {
return function(req, res, cb) {
const parseURL = require('url').parse;
const { allLanguages } = config.translation;
const { pathname, search } = parseURL(req.url);
allLanguages.forEach(lng => {
if (pathname === `/${lng}`) {
res.redirect(301, pathname.replace(`/${lng}`, `/${lng}/`) + (search || ''));
}
});
cb();
};
}
function makeConfig() {
const PORT = process.env.PORT || 3000;
const DEFAULT_LANGUAGE = 'en';
const OTHER_LANGUAGES = ['vi'];
const DEFAULT_NAMESPACE = 'common';
const LOCALE_PATH = 'static/locales';
const LOCALE_STRUCTURE = '{{lng}}/{{ns}}';
const LOCALE_SUBPATHS = false;
/* Core Settings - only change if you understand what you're changing */
const config = {
port: PORT,
translation: {
allLanguages: OTHER_LANGUAGES.concat([DEFAULT_LANGUAGE]),
defaultLanguage: DEFAULT_LANGUAGE,
fallbackLng: process.env.NODE_ENV === 'production' ? DEFAULT_LANGUAGE : null,
load: 'languageOnly',
localesPath: `./${LOCALE_PATH}/`,
localeSubpaths: LOCALE_SUBPATHS,
ns: [DEFAULT_NAMESPACE],
defaultNS: DEFAULT_NAMESPACE,
interpolation: {
escapeValue: false,
formatSeparator: ',',
format: (value, format) => (format === 'uppercase' ? value.toUpperCase() : value),
},
detection: {
order: ['cookie', 'header', 'querystring'],
caches: ['cookie'],
},
backend: {
loadPath: `/${LOCALE_PATH}/${LOCALE_STRUCTURE}.json`,
addPath: `/${LOCALE_PATH}/${LOCALE_STRUCTURE}.missing.json`,
},
},
};
/* SSR Settings - only change if you understand what you're changing */
const fs = require('fs');
const path = require('path');
const getAllNamespaces = p => fs.readdirSync(p).map(file => file.replace('.json', ''));
config.translation = {
...config.translation,
preload: config.translation.allLanguages,
ns: getAllNamespaces(`${config.translation.localesPath}${config.translation.defaultLanguage}`),
backend: {
loadPath: path.join(__dirname, `${LOCALE_PATH}/${LOCALE_STRUCTURE}.json`),
addPath: path.join(__dirname, `${LOCALE_PATH}/${LOCALE_STRUCTURE}.missing.json`),
},
};
return config
}
function registerI18n(server) {
const i18next = require('i18next');
const config = makeConfig();
const i18nextNodeBackend = require('i18next-node-fs-backend');
const i18nextMiddleware = require('i18next-express-middleware');
const i18n = i18next.default ? i18next.default : i18next;
i18n.nsFromReactTree = [];
const { allLanguages, localeSubpaths } = config.translation;
i18n.use(i18nextNodeBackend).use(i18nextMiddleware.LanguageDetector);
server.use(i18nextMiddleware.handle(i18n));
if (localeSubpaths) {
server.get('*', forceTrailingSlash(config));
server.get(/^\/(?!_next|static).*$/, lngPathDetector);
server.get(`/:lng(${allLanguages.join('|')})/*`, (req, res) => {
const { lng } = req.params;
app.render(req, res, req.url.replace(`/${lng}`, ''), { lng });
});
}
if (!i18n.isInitialized) {
i18n.init(config.translation);
}
}
module.exports = registerI18n
import React from 'react';
import {i18n, withNamespaces} from 'i18n-client';
class LocaleSwitcher extends React.Component {
render() {
const { t } = this.props;
return (
<button
type="submit"
onClick={() => i18n.changeLanguage(i18n.language.startsWith('en') ? 'de' : 'en')}
>
{t('common:change-locale')}
</button>
);
}
}
export default withNamespaces(['common'])(LocaleSwitcher);
const express = require('express');
const next = require('next');
const app = next({ dev: process.env.NODE_ENV !== 'production' });
const handle = app.getRequestHandler();
(async () => {
await app.prepare();
const server = express();
const registerI18n = require('i18n-server')
registerI18n(server)
server.get('*', (req, res) => handle(req, res));
await server.listen(3000);
console.log(`> Ready on http://localhost:3000`);
})();
@Vaibhav-Agarwal06
Copy link

Vaibhav-Agarwal06 commented Aug 1, 2019

I have implemented SSR rendering as per above example all work well but there is small problem when page render on client site then the translation does not work and revert the server translated text to original text. Can you please help?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment