diff --git a/examples/with-react-i18next/README.md b/examples/with-react-i18next/README.md new file mode 100644 index 00000000..34498cb4 --- /dev/null +++ b/examples/with-react-i18next/README.md @@ -0,0 +1,51 @@ +# Getting started + +Example with [react-i18next](https://github.com/i18next/react-i18next). + +```bash +# npm install +# npm run dev +``` + +**open:** + +auto detecting user language: [http://localhost:3000](http://localhost:3000) + +german: [http://localhost:3000/?lng=de](http://localhost:3000/?lng=de) + +english: [http://localhost:3000/?lng=en](http://localhost:3000/?lng=en) + + +## The idea behind the example + +This example app shows how to integrate [react-i18next](https://github.com/i18next/react-i18next) with [Next](https://github.com/zeit/next.js). + +**Plus:** + +- Routing and separating translations into multiple files (lazy load them on client routing) +- Child components (pure or using translation hoc) + +### Features of this example app + +- Server-side language negotiation +- Full control and usage of i18next on express server using [i18next-express-middleware](https://github.com/i18next/i18next-express-middleware) which asserts no async request collisions resulting in wrong language renderings +- Support for save missing features to get untranslated keys automatically created `locales/{lng}/{namespace}.missing.json` -> never miss to translate a key +- Proper pass down on translations via initialProps +- Taking advantage of multiple translation files including lazy loading on client (no need to load all translations upfront) +- Use express to also serve translations for clientside +- In contrast to react-intl the translations are visible both during development and in production + +### learn more + +- [next.js](https://github.com/zeit/next.js) +- [react-i18next repository](https://github.com/i18next/react-i18next) +- [react-i18next documentation](https://react.i18next.com) + +**Translation features:** + +- [i18next repository](https://github.com/i18next/i18next) +- [i18next documentation](https://www.i18next.com) + +**Translation management:** + +- [locize](http://locize.com) diff --git a/examples/with-react-i18next/components/ExtendedComponent.js b/examples/with-react-i18next/components/ExtendedComponent.js new file mode 100644 index 00000000..742b4b5a --- /dev/null +++ b/examples/with-react-i18next/components/ExtendedComponent.js @@ -0,0 +1,14 @@ +import React from 'react' +import { translate } from 'react-i18next' + +function MyComponennt ({ t }) { + return ( +
+ {t('extendedComponent')} +
+ ) +} + +const Extended = translate('common')(MyComponennt) + +export default Extended diff --git a/examples/with-react-i18next/components/PureComponent.js b/examples/with-react-i18next/components/PureComponent.js new file mode 100644 index 00000000..636efcdc --- /dev/null +++ b/examples/with-react-i18next/components/PureComponent.js @@ -0,0 +1,10 @@ +import React from 'react' + +// pure component just getting t function by props +export default function PureComponent ({ t }) { + return ( +
+ {t('common:pureComponent')} +
+ ) +} diff --git a/examples/with-react-i18next/i18n.js b/examples/with-react-i18next/i18n.js new file mode 100644 index 00000000..ef715d56 --- /dev/null +++ b/examples/with-react-i18next/i18n.js @@ -0,0 +1,59 @@ +const i18n = require('i18next') +const XHR = require('i18next-xhr-backend') +const LanguageDetector = require('i18next-browser-languagedetector') + +const options = { + fallbackLng: 'en', + load: 'languageOnly', // we only provide en, de -> no region specific locals like en-US, de-DE + + // have a common namespace used around the full app + ns: ['common'], + defaultNS: 'common', + + debug: true, + saveMissing: true, + + interpolation: { + escapeValue: false, // not needed for react!! + formatSeparator: ',', + format: (value, format, lng) => { + if (format === 'uppercase') return value.toUpperCase() + return value + } + } +} + +// for browser use xhr backend to load translations and browser lng detector +if (process.browser) { + i18n + .use(XHR) + // .use(Cache) + .use(LanguageDetector) +} + +// initialize if not already initialized +if (!i18n.isInitialized) i18n.init(options) + +// a simple helper to getInitialProps passed on loaded i18n data +i18n.getInitialProps = (req, namespaces) => { + if (!namespaces) namespaces = i18n.options.defautlNS + if (typeof namespaces === 'string') namespaces = [namespaces] + + 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 = i18n diff --git a/examples/with-react-i18next/locales/de/common.json b/examples/with-react-i18next/locales/de/common.json new file mode 100644 index 00000000..365aea8b --- /dev/null +++ b/examples/with-react-i18next/locales/de/common.json @@ -0,0 +1,5 @@ +{ + "integrates_react-i18next": "Dieses Beispiel integriert react-i18next für einfache Übersetzung.", + "pureComponent": "Entweder t Funktion an Komponente via props weiterreichen.", + "extendedComponent": "Oder die Komponente erneut mit dem translate hoc erweiteren." +} diff --git a/examples/with-react-i18next/locales/de/home.json b/examples/with-react-i18next/locales/de/home.json new file mode 100644 index 00000000..689b8237 --- /dev/null +++ b/examples/with-react-i18next/locales/de/home.json @@ -0,0 +1,6 @@ +{ + "welcome": "Willkommen zu next.js", + "link": { + "gotoPage2": "Zur Seite 2" + } +} diff --git a/examples/with-react-i18next/locales/de/page2.json b/examples/with-react-i18next/locales/de/page2.json new file mode 100644 index 00000000..398b0658 --- /dev/null +++ b/examples/with-react-i18next/locales/de/page2.json @@ -0,0 +1,6 @@ +{ + "welcomePage2": "Dies ist die 2te Seite", + "link": { + "gotoPage1": "Zur Seite 1" + } +} diff --git a/examples/with-react-i18next/locales/en/common.json b/examples/with-react-i18next/locales/en/common.json new file mode 100644 index 00000000..21d38300 --- /dev/null +++ b/examples/with-react-i18next/locales/en/common.json @@ -0,0 +1,5 @@ +{ + "integrates_react-i18next": "this sample integrates react-i18next for simple internationalization.", + "pureComponent": "You can either pass t function to child components.", + "extendedComponent": "Or wrap your component using the translate hoc provided by react-i18next." +} diff --git a/examples/with-react-i18next/locales/en/home.json b/examples/with-react-i18next/locales/en/home.json new file mode 100644 index 00000000..b4870bde --- /dev/null +++ b/examples/with-react-i18next/locales/en/home.json @@ -0,0 +1,6 @@ +{ + "welcome": "welcome to next.js", + "link": { + "gotoPage2": "Go to page 2" + } +} diff --git a/examples/with-react-i18next/locales/en/page2.json b/examples/with-react-i18next/locales/en/page2.json new file mode 100644 index 00000000..56397201 --- /dev/null +++ b/examples/with-react-i18next/locales/en/page2.json @@ -0,0 +1,6 @@ +{ + "welcomePage2": "this is page 2", + "link": { + "gotoPage1": "Back to page 1" + } +} diff --git a/examples/with-react-i18next/package.json b/examples/with-react-i18next/package.json new file mode 100644 index 00000000..c8116869 --- /dev/null +++ b/examples/with-react-i18next/package.json @@ -0,0 +1,25 @@ +{ + "name": "react-i18next-nextjs-example", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "dev": "node server.js", + "build": "next build", + "start": "NODE_ENV=production node server.js" + }, + "author": "", + "license": "MIT", + "dependencies": { + "express": "4.15.3", + "i18next": "8.4.2", + "i18next-browser-languagedetector": "2.0.0", + "i18next-express-middleware": "1.0.5", + "i18next-node-fs-backend": "1.0.0", + "i18next-xhr-backend": "1.4.2", + "next": "2.4.4", + "react": "15.6.1", + "react-dom": "15.6.1", + "react-i18next": "4.6.3" + } +} diff --git a/examples/with-react-i18next/pages/index.js b/examples/with-react-i18next/pages/index.js new file mode 100644 index 00000000..3fb70fab --- /dev/null +++ b/examples/with-react-i18next/pages/index.js @@ -0,0 +1,30 @@ +import React from 'react' +import Link from 'next/link' +import { translate } from 'react-i18next' +import i18n from '../i18n' + +import PureComponent from '../components/PureComponent' +import ExtendedComponent from '../components/ExtendedComponent' + +function Home ({ t, initialI18nStore }) { + return ( +
+ {t('welcome')} +

{t('common:integrates_react-i18next')}

+ + + {t('link.gotoPage2')} +
+ ) +} + +const Extended = translate(['home', 'common'], { i18n, wait: process.browser })(Home) + +// Passing down initial translations +// use req.i18n instance on serverside to avoid overlapping requests set the language wrong +Extended.getInitialProps = async ({ req }) => { + if (req && !process.browser) return i18n.getInitialProps(req, ['home', 'common']) + return {} +} + +export default Extended diff --git a/examples/with-react-i18next/pages/page2.js b/examples/with-react-i18next/pages/page2.js new file mode 100644 index 00000000..88da2764 --- /dev/null +++ b/examples/with-react-i18next/pages/page2.js @@ -0,0 +1,30 @@ +import React from 'react' +import Link from 'next/link' +import { translate } from 'react-i18next' +import i18n from '../i18n' + +import PureComponent from '../components/PureComponent' +import ExtendedComponent from '../components/ExtendedComponent' + +function Page2 ({ t, initialI18nStore }) { + return ( +
+ {t('welcomePage2')} +

{t('common:integrates_react-i18next')}

+ + + {t('link.gotoPage1')} +
+ ) +} + +const Extended = translate(['page2', 'common'], { i18n, wait: process.browser })(Page2) + +// Passing down initial translations +// use req.i18n instance on serverside to avoid overlapping requests set the language wrong +Extended.getInitialProps = async ({ req }) => { + if (req && !process.browser) return i18n.getInitialProps(req, ['page2', 'common']) + return {} +} + +export default Extended diff --git a/examples/with-react-i18next/server.js b/examples/with-react-i18next/server.js new file mode 100644 index 00000000..934f9098 --- /dev/null +++ b/examples/with-react-i18next/server.js @@ -0,0 +1,48 @@ +const express = require('express') +const path = require('path') +const next = require('next') + +const dev = process.env.NODE_ENV !== 'production' +const app = next({ dev }) +const handle = app.getRequestHandler() + +const i18nextMiddleware = require('i18next-express-middleware') +const Backend = require('i18next-node-fs-backend') +const i18n = require('./i18n') + +// init i18next with serverside settings +// using i18next-express-middleware +i18n + .use(Backend) + .use(i18nextMiddleware.LanguageDetector) + .init({ + preload: ['en', 'de'], // preload all langages + ns: ['common', 'home', 'page2'], // need to preload all the namespaces + backend: { + loadPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.json'), + addPath: path.join(__dirname, '/locales/{{lng}}/{{ns}}.missing.json') + } + }, () => { + // loaded translations we can bootstrap our routes + app.prepare() + .then(() => { + const server = express() + + // enable middleware for i18next + server.use(i18nextMiddleware.handle(i18n)) + + // serve locales for client + server.use('/locales', express.static(path.join(__dirname, '/locales'))) + + // missing keys + server.post('/locales/add/:lng/:ns', i18nextMiddleware.missingKeyHandler(i18n)) + + // use next.js + server.get('*', (req, res) => handle(req, res)) + + server.listen(3000, (err) => { + if (err) throw err + console.log('> Ready on http://localhost:3000') + }) + }) + })