gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.27k stars 10.31k forks source link

Gatsby build issue with react-i18next (SSR?) #25232

Closed samOpenforce closed 4 years ago

samOpenforce commented 4 years ago

Building a Gatsby application and using react-i18next. Implemented the withTranslation() approach and wrapped our layout component with i18NextProvider, layout in turn wraps all other components. Project builds fine in dev but in prod (SSR) we receive the following browser error:

react-i18next:: You will need pass in an i18next instance by using initReactI18next

Using this older guide to try to cater to SSR requirements but even with updating the code it's not working as expected. Build does not fail but app in browser does.

Any help or pointers gratefully received!

Key files

i18n.js

import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import translationEN from '../public/locales/en/translation.json';
import translationDE from '../public/locales/de/translation.json';

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    initImmediate: false,
    lng: 'de',
    fallbackLng: 'de',
    resources: {
      de: {
        translation: translationDE,
      },
      en: {
        translation: translationEN,
      },
    },

    // have a common namespace used around the full app
    ns: ['translation'],
    defaultNS: 'translation',

    debug: true,

    interpolation: {
      escapeValue: false, // not needed for react!!
    },
  });

export default i18n;

layout.js

import { graphql, StaticQuery } from 'gatsby';
import React from 'react';
import { Container } from 'react-bootstrap';
import { withTranslation, I18nextProvider } from 'react-i18next';
import Header from './Header';
import Footer from './Footer';
import ScrollToTop from './utils/ScrollToTop';
import SiteSeo from './utils/SiteSeo';

const Layout = ({ children, t, i18n }) => (
  <StaticQuery
    query={graphql`
      query SiteTitleQuery {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={data => (
      <>
        <I18nextProvider i18n={i18n}>
          <SiteSeo
            title={data.site.siteMetadata.title}
            meta={[
              { name: 'description', content: 'Sample' },
              { name: 'keywords', content: 'sample, something' },
            ]}
          >
            <html lang="de" />
          </SiteSeo>
          <div className="site">
            <Header />
            <div className="site-content">
              <Container>{children}</Container>
              <ScrollToTop></ScrollToTop>
            </div>
            <Footer />
          </div>
        </I18nextProvider>
      </>
    )}
  />

Environment (if relevant)


 System:
    OS: macOS Mojave 10.14.6
    CPU: (8) x64 Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
    Shell: 5.3 - /bin/zsh
  Binaries:
    Node: 14.2.0 - ~/.nvm/versions/node/v14.2.0/bin/node
    npm: 6.14.4 - ~/.nvm/versions/node/v14.2.0/bin/npm
  Languages:
    Python: 2.7.10 - /usr/bin/python
  Browsers:
    Chrome: 83.0.4103.106
    Firefox: 76.0.1
    Safari: 12.1.2
  npmPackages:
    gatsby: ^2.19.45 => 2.21.17
    gatsby-background-image: ^1.1.1 => 1.1.1
    gatsby-image: ^2.3.2 => 2.4.3
    gatsby-plugin-gtag: ^1.0.13 => 1.0.13
    gatsby-plugin-i18n: ^1.0.1 => 1.0.1
    gatsby-plugin-layout: ^1.2.1 => 1.3.1
    gatsby-plugin-react-helmet: ^3.2.1 => 3.3.1
    gatsby-plugin-react-svg: ^3.0.0 => 3.0.0
    gatsby-plugin-sass: ^2.2.1 => 2.3.1
    gatsby-plugin-sharp: ^2.5.4 => 2.6.2
    gatsby-plugin-styled-components: ^3.3.0 => 3.3.1
    gatsby-remark-images: ^3.2.3 => 3.3.1
    gatsby-source-filesystem: ^2.2.2 => 2.3.1
    gatsby-source-strapi: file:./lib/gatsby-source-strapi-0.0.12.tgz => 0.0.12
    gatsby-transformer-remark: ^2.7.1 => 2.8.7
    gatsby-transformer-sharp: ^2.4.4 => 2.5.2

File contents (if changed)

gastby-config.js
---
const activeEnv =
  process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || 'development';

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
});

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/static/images/`,
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `blog-images`,
        path: `${__dirname}/src/pages/blog/blog-images`,
      },
    },
    `gatsby-remark-images`,
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 200,
            },
          },
        ],
      },
    },
    { resolve: `gatsby-transformer-sharp` },
    { resolve: `gatsby-plugin-sharp` },
    { resolve: `gatsby-background-image` },
    { resolve: `gatsby-plugin-styled-components` },
    { resolve: `gatsby-plugin-sass` },
    { resolve: 'gatsby-plugin-react-helmet' },
    {
      resolve: 'gatsby-plugin-react-svg',
      options: {
        rule: {
          include: /assets/, // See https://www.gatsbyjs.org/packages/gatsby-plugin-react-svg/
        },
      },
    },
    {
      resolve: `gatsby-plugin-gtag`,
      options: {
        trackingId: process.env.GA_TRACKING_ID,
        head: false,
        anonymize: true,
        respectDNT: true,
      },
    },
    {
      resolve: `gatsby-plugin-layout`,
      options: {
        component: `${__dirname}/src/components/Layout.js`,
      },
    },
    {
      resolve: 'gatsby-source-strapi',
      options: {
        apiURL: process.env.API_URL || 'http://localhost:1337',
        contentTypes: [
          // List of the Content Types you want to be able to request from Gatsby.
          'start-page',
          'consultations',
          'job-posts',
          'employee-profile',
          'career-page',
          'job-page',
          'application-form',
        ],
        queryLimit: 1000,
      },
    },
    /*
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: "gatsby-starter-default",
        short_name: "starter",
        start_url: "/",
        background_color: "#663399",
        theme_color: "#663399",
        display: "minimal-ui",
      },
    },
    "gatsby-plugin-offline",
*/
    {
      resolve: 'gatsby-plugin-i18n',
      options: {
        langKeyDefault: 'de',
        useLangKeyLayout: false,
        markdownRemark: {
          postPage: 'src/templates/blog-post.js',
          query: `
            {
              allMarkdownRemark {
                edges {
                  node {
                    fields {
                      slug,
                      langKey
                    }
                  }
                }
              }
            }
          `,
        },
      },
    },
  ],
  siteMetadata: {
    title: 'openForce Information Technology GesmbH ',
    titleTemplate: '%s · Cloud Natives',
    description:
      'Wir organisieren rasch und sicher den Umzug Ihrer IT-Systeme in die Cloud. Experten in der Softwareentwicklung unterstützen Sie mit individuellen Lösungen über alle gängigen Plattformen und Endgeräte hinweg.',
    url: 'localhost:8000', // No trailing slash allowed!
    image: '/static/images/openforce_logo.png', // Path to your image you placed in the 'static' folder
    twitterUsername: '@openForceCom',
    defaultLang: 'de',
  },
};
package.json
---
{
  "name": "openforce.com",
  "private": true,
  "description": "OpenFroce customer facing website",
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {
    "build": "gatsby build",
    "develop": "gatsby clean && gatsby develop && curl -X POST http://localhost:8000/__refresh",
    "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
    "start": "npm run develop",
    "serve": "gatsby serve",
    "clean": "gatsby clean",
    "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
  },
  "dependencies": {
    "babel-plugin-styled-components": "^1.10.7",
    "bootstrap": "^4.4.1",
    "gatsby": "^2.19.45",
    "gatsby-background-image": "^1.1.1",
    "gatsby-image": "^2.3.2",
    "gatsby-plugin-gtag": "^1.0.13",
    "gatsby-plugin-i18n": "^1.0.1",
    "gatsby-plugin-layout": "^1.2.1",
    "gatsby-plugin-react-helmet": "^3.2.1",
    "gatsby-plugin-react-svg": "^3.0.0",
    "gatsby-plugin-sass": "^2.2.1",
    "gatsby-plugin-sharp": "^2.5.4",
    "gatsby-plugin-styled-components": "^3.3.0",
    "gatsby-remark-images": "^3.2.3",
    "gatsby-source-filesystem": "^2.2.2",
    "gatsby-source-strapi": "file:./lib/gatsby-source-strapi-0.0.12.tgz",
    "gatsby-transformer-remark": "^2.7.1",
    "gatsby-transformer-sharp": "^2.4.4",
    "i18next": "^19.3.4",
    "i18next-browser-languagedetector": "^4.0.2",
    "i18next-http-backend": "^1.0.15",
    "i18next-xhr-backend": "^3.2.2",
    "knex": "^0.21.0",
    "node-sass": "^4.13.1",
    "react": "^16.3.0",
    "react-bootstrap": "^1.0.0",
    "react-dom": "^16.3.0",
    "react-dropzone": "^11.0.1",
    "react-helmet": "^5.2.1",
    "react-i18next": "^11.3.4",
    "react-markdown": "^4.3.1",
    "reaptcha": "^1.7.2",
    "sqlite3": "^4.1.1",
    "strapi": "^3.0.0-beta.20",
    "strapi-admin": "^3.0.0-beta.20",
    "strapi-connector-bookshelf": "^3.0.0-beta.20",
    "strapi-plugin-content-manager": "^3.0.0-beta.20",
    "strapi-plugin-content-type-builder": "^3.0.0-beta.20",
    "strapi-plugin-email": "^3.0.0-beta.20",
    "strapi-plugin-graphql": "^3.0.0",
    "strapi-plugin-upload": "^3.0.0-beta.20",
    "strapi-plugin-users-permissions": "^3.0.0-beta.20",
    "styled-components": "^5.1.0"
  },
  "devDependencies": {
    "prettier": "^1.19.1"
  }
}
gatsby-node.js
---
const path = require(`path`);
const { createFilePath } = require(`gatsby-source-filesystem`);
const fs = require('fs-extra');

exports.onPostBuild = () => {
  console.log('Copying locales');
  fs.copySync(
    path.join(__dirname, '/static/locales'),
    path.join(__dirname, '/public/locales')
  );
};

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  const result = await graphql(
    `
      {
        allStrapiJobPosts {
          edges {
            node {
              title
              language {
                language
              }
              id
              strapiId
            }
          }
        }
      }
    `
  );

  if (result.errors) {
    throw result.errors;
  }

  const jobs = result.data.allStrapiJobPosts.edges;
  jobs.forEach((job, index) => {
    const $title = `${job.node.title}`;
    const $titleArray = $title.split(' ');
    const $titleAsPath = $titleArray.join('-');
    const pathText =
      `${job.node.language.language}` === 'de' ? 'karriere' : 'careers';

    createPage({
      path: `${job.node.language.language}/${pathText}/${$titleAsPath}`,
      component: require.resolve('./src/templates/job-post.js'),
      context: {
        id: job.node.strapiId,
      },
    });
  });
};
gatsby-browser.js
---
import "./node_modules/bootstrap/dist/css/bootstrap.css"
import "./src/styles/global.scss"
gatsby-ssr.js
---
import React from 'react';
import { renderToString } from 'react-dom/server';
import i18n from './src/i18n';
const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {
  i18n.loadNamespaces(['translation'], () => {
    replaceBodyHTMLString(bodyComponent);
  });
};

module.exports.replaceRenderer;
jzabala commented 4 years ago

Sorry if I am asking a dump question but how exactly does the Layout gets the i18n instance?

From what I see Layout is expecting an i18n prop (const Layout = ({ children, t, i18n })), but the it is rendered by gatsby-plugin-layout, so how is it getting the prop?

LekoArts commented 4 years ago

Thank you for opening this!

While this isn't directly related to Gatsby but more so about the correct implementation in your React code I'll give you some pointers to unblock you.

Firstly, the article you linked is pretty outdated, you can find articles like https://itnext.io/techniques-approaches-for-multi-language-gatsby-apps-8ba13ff433c5 via Google which should give you a better guidance.

You're also using multiple ways at the same time to access the i18n data. You shouldn't use the HOC, initReactContext and the Provider at the same time. Using the Provider alone is enough. This is what I'm currently working on: https://github.com/LekoArts/gatsby-theme-i18n/blob/7c7cb34df20a60d11906bfd7964c1515d0c27fce/packages/gatsby-theme-i18n-react-i18next/src/wrap-page-element.js - it's only passing the i18n instance to the Provider and then you can use the useTranslation hook (https://react.i18next.com/latest/usetranslation-hook) to access it. Using the Provider is necessary to have SSR working.

Lastly, you can use https://www.gatsbyjs.org/packages/gatsby-plugin-react-i18next/ if you don't want to implement the Provider logic yourself.

We're marking this issue as answered and closing it for now but please feel free to comment here if you would like to continue this discussion. We also recommend heading over to our communities if you have questions that are not bug reports or feature requests. We hope we managed to help and thank you for using Gatsby!

samOpenforce commented 4 years ago

@LekoArts Thank you for the response and the tips. I will work through the information you've kindly provided.

ghost commented 4 years ago

@samOpenforce Though still quite helpful I too tripped up with the guide you linked in OP. While following that post everything seems to go smoothly until the very end where it says Not tested. Anyway, here's an i18n starter I threw together in case it helps:

https://git.habd.as/comfusion/gatsby-starter-i18n-react-i18next

I'm able to run gatsby build and see translations using gatsby serve just fine. A cursory glance at the code shows my example using the i18n provider in gatsby-browser and not in the layout. I'm also not using the replaceRenderer stuff in gatsby-ssr as that (in addition to page generation by locale, depending on individual requirements) seems to be the missing piece to getting a more search-friendly internationalized Gatsby site put together.

samOpenforce commented 4 years ago

@balibebas Thanks for the update on your project.

I've managed to solve my issue based on advice from @LekoArts, as well as a thorough re-reading of the react-i18next docs now I have a better understanding of what the plugin is doing. Refactored to only use the i18nProvider and useTranslation() hook and everything is working as expected.

amitkrgupta094 commented 4 years ago

Hi @balibebas @samOpenforce .

Thank you for discussion, Gave me some idea how to unblock myself. I also got tripped off by the Post on gatsby. One small issue though, importing these json files inside of i81next initiation makes the initial build app.js file bigger packet size and it gets added there. is there a way to lazy load these json files seeing (React Suspense does not work here).

Thanks.

samOpenforce commented 4 years ago

hey @Amilight

I've not actually done this personally as our translation files are still small enough to serve in our initial load, however I think this section of the docs describes your situation Seperating translation files (or at least it lets you know what you want to do is possible).

amitkrgupta094 commented 4 years ago

Hey @samOpenforce . Thank you for getting back to me. I have about 8 pages with 11 languages each. Now we are planning to go with one page's translation. So as per I understand now - 1 - We create the i18n instance and pass everything before hand (resources and configs & dependencies) 2 - Create a wrapper component with this i18n instance injected. 3 - Exposing this wrapper on top of our app using SSR.

(Please correct if I'm wrong. )

Problem: By doing this, the Json files gets added directly to app.js packet. So in my case -> It went from 37kB (gzip) --> 197kB(gzip).

I'm going through the link provided (Thank you for that :) ) by you and checking if I can add these resources to i18n instance on component level.

Update [27 September]: I tried implementing by directly putting json files inside of static folder and It stopped getting added for all pages Thus app.js bundle became small. Reference: Using Static Folder

Problem: I found the Json files started loading from static folder (tested in preprod) but switching of languages was not smooth anymore.

Probable solution(s) - I found this i18n solution by @LekoArts But I still have to evaluate the scalability of this approach.