Last active
August 1, 2019 15:35
-
-
Save revskill10/0f84ff38411a6254efb09be998572798 to your computer and use it in GitHub Desktop.
React-i18next
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
This `Link` component is a wrap of the standard | |
NextJs `Link` component, with some simple lang | |
redirect logic in place. | |
If you haven't already, read this issue comment: | |
https://github.com/zeit/next.js/issues/2833#issuecomment-414919347 | |
This component automatically provides this functionality: | |
<Link href="/product?slug=something" as="/products/something"> | |
Wherein `slug` is actually our i18n lang, and it gets | |
pulled automatically. | |
Very important: if you import `Link` from NextJs directly, | |
and not this file, your lang subpath routing will break. | |
*/ | |
import React from 'react'; | |
import NextLink from 'next/link'; | |
import {i18n, withNamespaces, config} from 'i18n/client'; | |
const {translation} = config | |
class Link extends React.Component { | |
render() { | |
const { children, href } = this.props; | |
const lng = i18n.languages[0]; | |
if (translation.localeSubpaths && lng !== translation.defaultLanguage) { | |
return ( | |
<NextLink href={`${href}?lng=${lng}`} as={`/${lng}${href}`}> | |
{children} | |
</NextLink> | |
); | |
} | |
return <NextLink href={href}>{children}</NextLink>; | |
} | |
} | |
/* | |
Usage of `withNamespaces` here is just to | |
force `Link` to rerender on language change | |
*/ | |
export default withNamespaces()(Link); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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?