├── .gitignore ├── README.md ├── components ├── active-link.js ├── banner.js ├── cards.js ├── footer.js ├── head.js ├── navigation.js └── user-context.js ├── config ├── env.js ├── public.runtime.js └── server.runtime.js ├── i18n.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _error.js ├── about.js └── index.js ├── public ├── css │ └── bootstrap.min.css ├── img │ ├── flags │ │ ├── en.svg │ │ └── fr.svg │ ├── thumb1.jpg │ ├── thumb2.jpg │ └── thumb3.jpg └── locales │ ├── en │ └── common.json │ └── fr │ └── common.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | certs/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using i18next with Next.js, Fastify and React context API 2 | 3 | This is a demo app that show a basic app build for internationalization with user cusom settings. 4 | You can find a articles that talk about that here : 5 | 6 | # installation 7 | 8 | ```shell 9 | npm install 10 | npm run generate-certs 11 | npm run dev 12 | ``` 13 | -------------------------------------------------------------------------------- /components/active-link.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Link with next-css are broken in dev. 3 | * follow this issue: https://github.com/zeit/next-plugins/issues/282 4 | * This is a workaround class 5 | */ 6 | import { useRouter } from "next/router"; 7 | import PropTypes from "prop-types"; 8 | import Link from "next/link"; 9 | import React, { Children } from "react"; 10 | 11 | const ActiveLink = ({ children, activeClassName, ...props }) => { 12 | const { pathname } = useRouter(); 13 | const child = Children.only(children); 14 | 15 | const childClassName = child.props.className ? child.props.className : ""; 16 | const className = pathname === props.href ? `${childClassName} ${activeClassName}` : childClassName; 17 | 18 | return {React.cloneElement(child, { href: props.href, className })}; 19 | }; 20 | 21 | ActiveLink.propTypes = { 22 | activeClassName: PropTypes.string.isRequired 23 | }; 24 | 25 | export default ActiveLink; 26 | -------------------------------------------------------------------------------- /components/banner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Typed from "react-typed"; 3 | import { Jumbotron, Button } from "react-bootstrap"; 4 | 5 | import UserContext from "./user-context"; 6 | import { withTranslation } from "../i18n"; 7 | 8 | import "react-typed/dist/animatedCursor.css"; 9 | 10 | class Banner extends Component { 11 | static contextType = UserContext; 12 | 13 | state = { 14 | arrJumbo: [""] 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | } 20 | 21 | updateTyped() { 22 | this.setState({ 23 | arrJumbo: [ 24 | this.props.t("common:banner.jumbotron.p1"), 25 | this.props.t("common:banner.jumbotron.p2"), 26 | this.props.t("common:banner.jumbotron.p3"), 27 | this.props.t("common:banner.jumbotron.p4") 28 | ] 29 | }); 30 | this.typed.reset(); 31 | } 32 | 33 | componentDidMount() { 34 | const { registerLangListener } = this.context; 35 | this.updateTyped(); 36 | registerLangListener(this.updateTyped, this); 37 | } 38 | 39 | componentWillUnmount() { 40 | const { unregisterLangListener } = this.context; 41 | unregisterLangListener(this.updateTyped, this); 42 | this.setState({ 43 | arrJumbo: [] 44 | }); 45 | } 46 | 47 | render() { 48 | return ( 49 | 50 |

51 | {" "} 52 | { 54 | this.typed = typed; 55 | }} 56 | className="typelist-skill" 57 | strings={this.state.arrJumbo} 58 | typeSpeed={40} 59 | backSpeed={50} 60 | backDelay={2000} 61 | loop 62 | /> 63 |

64 |

{this.props.t("common:banner.subtitle")}

65 |

66 | 69 |

70 |
71 | ); 72 | } 73 | } 74 | 75 | export default withTranslation("common")(Banner); 76 | -------------------------------------------------------------------------------- /components/cards.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Row, Col, Card, Button } from "react-bootstrap"; 3 | 4 | import { withTranslation } from "../i18n"; 5 | 6 | class Cards extends Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | {this.props.t("common:cards.c1.title")} 15 | {this.props.t("common:cards.c1.text")} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {this.props.t("common:cards.c2.title")} 26 | {this.props.t("common:cards.c2.text")} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {this.props.t("common:cards.c3.title")} 37 | {this.props.t("common:cards.c3.text")} 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | export default withTranslation("common")(Cards); 47 | -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default class Footer extends Component { 4 | render() { 5 | return
; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /components/head.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextHead from "next/head"; 3 | import { string } from "prop-types"; 4 | 5 | const defaultDescription = "i18n example with Next.js & Fastify"; 6 | 7 | const Head = props => ( 8 | 9 | 10 | {props.title || ""} 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | Head.propTypes = { 19 | title: string, 20 | description: string 21 | }; 22 | 23 | export default Head; 24 | -------------------------------------------------------------------------------- /components/navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Navbar, Nav } from "react-bootstrap"; 3 | 4 | // workaround broken Link with next-css 5 | // https://github.com/zeit/next-plugins/issues/282 6 | import ActiveLink from "./active-link"; 7 | 8 | import UserContext from "./user-context"; 9 | import { withTranslation } from "../i18n"; 10 | 11 | class Navigation extends Component { 12 | static contextType = UserContext; 13 | 14 | constructor(props) { 15 | super(props); 16 | } 17 | 18 | toggleLang() { 19 | const { toggleLang } = this.context; 20 | toggleLang(); 21 | } 22 | 23 | render() { 24 | return ( 25 | <> 26 | 27 | Navbar 28 | 36 |
37 | 43 | {this.context.lang === "fr" ? ( 44 | lang 45 | ) : ( 46 | lang 47 | )} 48 | 49 |
50 |
51 | 52 | ); 53 | } 54 | } 55 | export default withTranslation("common")(Navigation); 56 | -------------------------------------------------------------------------------- /components/user-context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import EventEmitter from "eventemitter3"; 3 | 4 | import { i18n } from "../i18n"; 5 | 6 | let UserContext = createContext({ 7 | lang: "en", 8 | toggleLang: () => {}, 9 | registerLangListener: () => {}, 10 | unregisterLangListener: () => {} 11 | }); 12 | 13 | export class UserProvider extends React.Component { 14 | state = { 15 | lang: "en", 16 | toggleLang: this.toggleLang.bind(this), 17 | registerLangListener: this.registerLangListener.bind(this), 18 | unregisterLangListener: this.unregisterLangListener.bind(this) 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | this.lnNotifier = new EventEmitter(); 24 | } 25 | 26 | detectBrowserlanguage(defaultLang) { 27 | let navLang = navigator.language || navigator.userLanguage || defaultLang; 28 | 29 | return navLang.substring(0, 2); 30 | } 31 | 32 | componentDidMount() { 33 | const defaultLang = this.detectBrowserlanguage("en"); 34 | const lang = localStorage.getItem("user-lang"); 35 | const usedLang = lang ? lang : defaultLang; 36 | 37 | this.setState( 38 | { 39 | lang: usedLang 40 | }, 41 | () => { 42 | i18n.changeLanguage(this.state.lang, () => { 43 | this.lnNotifier.emit("langChanged"); 44 | }); 45 | } 46 | ); 47 | } 48 | 49 | toggleLang() { 50 | this.setState({ lang: this.state.lang === "en" ? "fr" : "en" }, () => { 51 | localStorage.setItem("user-lang", this.state.lang); 52 | i18n.changeLanguage(this.state.lang, () => { 53 | this.lnNotifier.emit("langChanged"); 54 | }); 55 | }); 56 | } 57 | 58 | registerLangListener(fn, ctx) { 59 | this.lnNotifier.on("langChanged", fn, ctx); 60 | } 61 | 62 | unregisterLangListener(fn, ctx) { 63 | this.lnNotifier.removeListener("langChanged", fn, ctx); 64 | } 65 | 66 | render() { 67 | i18n.changeLanguage(this.state.lang); 68 | return {this.props.children}; 69 | } 70 | } 71 | 72 | export const UserConsumer = UserContext.Consumer; 73 | export default UserContext; 74 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const envCommon = {}; 2 | 3 | const envConfig = { 4 | development: {}, 5 | testing: {}, 6 | production: { 7 | IS_PROD: true 8 | } 9 | }; 10 | 11 | const currentEnv = process.env.NODE_ENV; 12 | module.exports = { ...envCommon, ...envConfig[currentEnv] }; 13 | -------------------------------------------------------------------------------- /config/public.runtime.js: -------------------------------------------------------------------------------- 1 | const env = require("./env"); 2 | 3 | /* Warning: 4 | Do not use process.env here, as this file is loaded from next.config.js and env is not setup yet by Next 5 | */ 6 | const publicConfig = { 7 | development: {}, 8 | testing: {}, 9 | production: {} 10 | }; 11 | 12 | /* Use process.env.NODE_ENV here as this one come from node ! */ 13 | const currentEnv = process.env.NODE_ENV; 14 | module.exports = publicConfig[currentEnv]; 15 | -------------------------------------------------------------------------------- /config/server.runtime.js: -------------------------------------------------------------------------------- 1 | const serverConfig = { 2 | development: {}, 3 | testing: {}, 4 | production: {} 5 | }; 6 | 7 | const currentEnv = process.env.NODE_ENV; 8 | module.exports = serverConfig[currentEnv]; 9 | -------------------------------------------------------------------------------- /i18n.js: -------------------------------------------------------------------------------- 1 | const NextI18Next = require("next-i18next").default; 2 | 3 | module.exports = new NextI18Next({ 4 | defaultLanguage: "en", 5 | otherLanguages: ["fr"], 6 | // workaround until next-i18next support public path 7 | // https://github.com/isaachinman/next-i18next/issues/523 8 | localePath: typeof window === "undefined" ? "public/locales" : "locales" 9 | }); 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const env = require("./config/env"); 2 | const publicConfig = require("./config/public.runtime"); 3 | const serverConfig = require("./config/server.runtime"); 4 | 5 | const nextConfig = { 6 | webpack: (config, options) => { 7 | // Fixes npm packages that depend on `fs` module 8 | config.node = { 9 | fs: "empty" 10 | }; 11 | return config; 12 | }, 13 | compress: false 14 | }; 15 | 16 | module.exports = () => { 17 | /* see https://github.com/zeit/next.js#build-time-configuration */ 18 | nextConfig.env = env; 19 | /* see https://github.com/zeit/next.js#runtime-configuration */ 20 | nextConfig.publicRuntimeConfig = publicConfig; 21 | nextConfig.serverRuntimeConfig = serverConfig; 22 | 23 | const withCSS = require("@zeit/next-css"); 24 | return withCSS(nextConfig); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-fastify-i18n", 3 | "version": "1.0.0", 4 | "description": "Example of i18n with fastify and next.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development node server.js", 8 | "build": "next build", 9 | "export": "next export", 10 | "clean": "cross-env rimraf ./.next ./out", 11 | "release": "cross-env NODE_ENV=production npm run clean && npm run build && npm run export", 12 | "start": "next start", 13 | "stage": "cross-env NODE_ENV=production node server.js", 14 | "generate-certs": "mkdir certs && openssl req -x509 -days 365 -newkey rsa:2048 -nodes -sha256 -keyout certs/privateKey.key -out certs/certificate.crt" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/typedef42/nextjs-fastify-i18n.git" 19 | }, 20 | "author": "Yannis Torres", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/typedef42/nextjs-fastify-i18n/issues" 24 | }, 25 | "homepage": "https://github.com/typedef42/nextjs-fastify-i18n#readme", 26 | "dependencies": { 27 | "@zeit/next-css": "^1.0.1", 28 | "eventemitter3": "^4.0.0", 29 | "fastify": "^2.10.0", 30 | "fastify-nextjs": "^4.1.1", 31 | "fastify-static": "^2.5.0", 32 | "i18next": "^19.0.0", 33 | "next": "^9.1.2", 34 | "next-i18next": "^2.0.0", 35 | "react": "^16.8.6", 36 | "react-bootstrap": "^1.0.0-beta.14", 37 | "react-dom": "^16.8.6", 38 | "react-typed": "^1.2.0" 39 | }, 40 | "devDependencies": { 41 | "cross-env": "^6.0.3", 42 | "rimraf": "^3.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from "next/app"; 3 | 4 | import { UserProvider } from "../components/user-context"; 5 | import { appWithTranslation } from "../i18n"; 6 | 7 | class TestApp extends App { 8 | render() { 9 | const { Component, pageProps } = this.props; 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | 20 | export default appWithTranslation(TestApp); 21 | -------------------------------------------------------------------------------- /pages/_error.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withTranslation } from "../i18n"; 5 | 6 | const Error = ({ statusCode, t }) => ( 7 |

{statusCode ? t("error-with-status", { statusCode }) : t("error-without-status")}

8 | ); 9 | 10 | Error.getInitialProps = async ({ res, err }) => { 11 | let statusCode = null; 12 | if (res) { 13 | ({ statusCode } = res); 14 | } else if (err) { 15 | ({ statusCode } = err); 16 | } 17 | return { 18 | namespacesRequired: ["common"], 19 | statusCode 20 | }; 21 | }; 22 | 23 | Error.defaultProps = { 24 | statusCode: null 25 | }; 26 | 27 | Error.propTypes = { 28 | statusCode: PropTypes.number, 29 | t: PropTypes.func.isRequired 30 | }; 31 | 32 | export default withTranslation("common")(Error); 33 | -------------------------------------------------------------------------------- /pages/about.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Head from "../components/head"; 4 | import Navigation from "../components/navigation"; 5 | 6 | import { withTranslation } from "../i18n"; 7 | 8 | class About extends React.Component { 9 | static async getInitialProps() { 10 | return { 11 | namespacesRequired: ["common"] 12 | }; 13 | } 14 | 15 | render() { 16 | return ( 17 | <> 18 | 19 | 20 |
21 |

{this.props.t("common:about.text")}

22 |
23 | 24 | ); 25 | } 26 | } 27 | 28 | export default withTranslation("common")(About); 29 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Head from "../components/head"; 4 | import Navigation from "../components/navigation"; 5 | import Footer from "../components/footer"; 6 | import Banner from "../components/banner"; 7 | import Cards from "../components/cards"; 8 | 9 | import { withTranslation } from "../i18n"; 10 | 11 | class Home extends React.Component { 12 | static async getInitialProps() { 13 | return { 14 | namespacesRequired: ["common"] 15 | }; 16 | } 17 | 18 | render() { 19 | return ( 20 | <> 21 | 22 | 23 |
24 | 25 | 26 |
27 |