gatsbyjs / gatsby

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

Translations (i18n) in child components won't work after hard refresh on production #15354

Closed benpixel closed 5 years ago

benpixel commented 5 years ago

Summary

I'm using a "custom" implementation of i18n with react-i18next, i18next-xhr-backend, etc because I needed a custom language detector. Here are the problems I have:

There is a <Newsletter /> functional component included in index.js and I'm using const { t } = useTranslation() to get translations. Now that works perfectly in development, on production it doesn't work on hard refresh or first load, only on page change. Doesn't work meaning it loads the fallback language. Same thing happens with <Card /> class component included in index.js and in it I'm using export default withTranslation()(Card) and then const { t } = this.props.

On index.js I'm using withTranslation just like in Card component and that's working perfectly, then I have contact.js setup in same way but the translations will use fallback language until I add any kind of graphql query on that page, then it will load the proper language.

What am I doing wrong? Do I need some kind of a i18n provider in my Layout? Do I need something in gatsby-ssr? Do I need to load locales via graphql somehow?

Relevant information

Layout.js:

import React, { Component } from "react"
import "components/i18n"

class Layout extends Component {
  render() {
    const { location, children } = this.props
    return <>{children}</>
  }
}

export default Layout

i18n.js:

import i18n from "i18next"
import Backend from "i18next-xhr-backend"
import LanguageDetector from "i18next-browser-languagedetector"
import { initReactI18next } from "react-i18next"

import generalDE from "../../locales/de/general.json"
import generalES from "../../locales/es/general.json"

const resources = {
  de: {
    general: generalDE,
  },
  es: {
    general: generalES,
  }
}

const CustomDetector = new LanguageDetector()
CustomDetector.addDetector({
  name: "customDetector",
  lookup(options) {
    if (typeof window !== "undefined") {
      const href = window.location.href
      const lng = localStorage.getItem("lng")
      if (href.includes("hamburg")) {
        return "de"
      } else if (href.includes("barcelona")) {
        return "es"
      } else if (lng) {
        return lng
      }
    }
  },
  cacheUserLanguage(lng) {
    if (typeof localStorage !== "undefined") {
      localStorage.setItem("lng", lng)
    }
  }
})

i18n
  .use(Backend)
  .use(CustomDetector)
  .use(initReactI18next)
  .init({
    resources,
    preload: ["de", "es"],

    detection: {
      order: ["customDetector"],
      caches: ["customDetector"]
    },

    fallbackLng: "de",
    whitelist: ["de", "es"],
    ns: ["general"],
    defaultNS: "general",

    interpolation: {
      escapeValue: false
    },

    react: {
      wait: true,
      useSuspense: false
    },

    debug: true
  })

export default i18n

Environment

  System:
    OS: macOS 10.14.3
    CPU: (4) x64 Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
    Shell: 5.3 - /bin/zsh
  Binaries:
    Node: 10.16.0 - ~/.nvm/versions/node/v10.16.0/bin/node
    Yarn: 1.16.0 - /usr/local/bin/yarn
    npm: 6.9.0 - ~/.nvm/versions/node/v10.16.0/bin/npm
  Languages:
    Python: 2.7.15 - /usr/local/bin/python
  Browsers:
    Chrome: 75.0.3770.100
    Firefox: 64.0
    Safari: 12.0.3
  npmPackages:
    gatsby: ^2.7.1 => 2.7.1 
    gatsby-background-image: ^0.7.0 => 0.7.0 
    gatsby-image: 2.1.0 => 2.1.0 
    gatsby-link: 2.1.1 => 2.1.1 
    gatsby-plugin-google-tagmanager: ^2.0.15 => 2.0.15 
    gatsby-plugin-manifest: ^2.1.1 => 2.1.1 
    gatsby-plugin-netlify: 2.0.17 => 2.0.17 
    gatsby-plugin-react-helmet: 3.0.12 => 3.0.12 
    gatsby-plugin-resolve-src: ^2.0.0 => 2.0.0 
    gatsby-plugin-sass: ^2.0.0-rc.2 => 2.0.11 
    gatsby-plugin-sharp: ^2.1.2 => 2.1.2 
    gatsby-remark-copy-linked-files: 2.0.12 => 2.0.12 
    gatsby-remark-images: 3.0.11 => 3.0.11 
    gatsby-source-apiserver: ^2.1.1 => 2.1.1 
    gatsby-source-filesystem: 2.0.37 => 2.0.37 
    gatsby-transformer-json: ^2.1.11 => 2.1.11 
    gatsby-transformer-remark: 2.3.12 => 2.3.12 
    gatsby-transformer-sharp: ^2.1.20 => 2.1.20 
  npmGlobalPackages:
    gatsby-cli: 2.7.7

File contents

gatsby-config.js:

module.exports = {
  siteMetadata: {
    title: "Testing",
    description: "",
    url: "https://testing.com",
    image: ""
  },
  plugins: [
    `gatsby-plugin-react-helmet`,
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    `gatsby-transformer-json`,
    `gatsby-plugin-sass`,
    // Manifest
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `stewart`,
        short_name: `testing`,
        start_url: `/`,
        background_color: `#000`,
        theme_color: `#fff`,
        display: `minimal-ui`,
        icon: `${__dirname}/src/assets/images/favicons/favicon.svg`,
        include_favicon: true
      }
    },
    // Get remote data
    {
      resolve: `gatsby-source-apiserver`,
      options: {
        typePrefix: ``,
        data: {},
        path: `${__dirname}/src/data/`,
        skipCreateNode: false,
        localSave: false,
        verboseOutput: true,
        name: `Listings`,
        url: `https://REDACTED`,
        method: "get",
        headers: {
          "Content-Type": "application/json"
        }
      }
    },
    // Filesystem
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `${__dirname}/src/data/`
      }
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `pages`,
        path: `${__dirname}/src/pages/`
      }
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/assets/images/`
      }
    },
    // Transformers
    {
      resolve: `gatsby-transformer-json`,
      options: {
        typeName: "Json"
      }
    },
    // Google Tag Manager
    {
      resolve: `gatsby-plugin-google-tagmanager`,
      options: {
        id: "REDACTED",
        includeInDevelopment: false
      }
    }
  ]
}

package.json:

{
  "name": "testing",
  "description": "",
  "version": "1.0.0",
  "dependencies": {
    "@sindresorhus/slugify": "^0.9.1",
    "axios": "^0.18.0",
    "classnames": "^2.2.6",
    "gatsby": "^2.7.1",
    "gatsby-background-image": "^0.7.0",
    "gatsby-image": "2.1.0",
    "gatsby-link": "2.1.1",
    "gatsby-plugin-google-tagmanager": "^2.0.15",
    "gatsby-plugin-manifest": "^2.1.1",
    "gatsby-plugin-netlify": "2.0.17",
    "gatsby-plugin-react-helmet": "3.0.12",
    "gatsby-plugin-resolve-src": "^2.0.0",
    "gatsby-plugin-sass": "^2.0.0-rc.2",
    "gatsby-plugin-sharp": "^2.1.2",
    "gatsby-remark-copy-linked-files": "2.0.12",
    "gatsby-remark-images": "3.0.11",
    "gatsby-source-apiserver": "^2.1.1",
    "gatsby-source-filesystem": "2.0.37",
    "gatsby-transformer-json": "^2.1.11",
    "gatsby-transformer-remark": "2.3.12",
    "gatsby-transformer-sharp": "^2.1.20",
    "i18next": "^17.0.1",
    "i18next-browser-languagedetector": "^3.0.1",
    "i18next-xhr-backend": "^3.0.0",
    "intersection-observer": "^0.7.0",
    "react": "^16.8.6",
    "react-cookie-banner": "^4.0.0",
    "react-cookie-consent": "^2.3.1",
    "react-dom": "^16.8.6",
    "react-helmet": "^5.2.1",
    "react-i18next": "^10.11.0",
    "react-image-lightbox": "^5.1.0",
    "react-slideshow-image": "^1.1.8",
    "react-star-ratings": "^2.3.0",
    "react-truncate": "^2.4.0"
  },
  "devDependencies": {
    "bootstrap": "4.3.1",
    "lodash": "4.17.11",
    "modern-normalize": "0.5.0",
    "node-sass": "4.12.0",
    "prettier": "^1.17.1"
  },
  "main": "n/a",
  "scripts": {
    "clean": "gatsby clean",
    "build": "gatsby build",
    "build:develop": "gatsby build",
    "build:production": "gatsby build",
    "deploy": "gatsby build --prefix-paths && gh-pages -d public",
    "develop": "gatsby develop"
  }
}

gatsby-node.js:

const Promise = require("bluebird")
const path = require("path")
const axios = require("axios")
const fs = require("fs-extra")
const { purposes } = require("./src/helpers")
const slugify = require("@sindresorhus/slugify")

function t(lang, key, master) {
  let obj
  if (master) {
    obj = require(`./src/locales/${lang}/${master}.json`)
  } else {
    obj = require(`./src/locales/${lang}/general.json`)
  }
  const label = obj[key]
  if (label) {
    return label
  } else {
    return null
  }
}

const langs = ["de", "es"]

require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`
})

// Locales
exports.onPostBuild = () => {
  console.log("Copying locales")
  fs.copySync(
    path.join(__dirname, "/src/locales"),
    path.join(__dirname, "/public/locales")
  )
}

// Get data
exports.sourceNodes = async ({
  actions,
  createNodeId,
  createContentDigest,
  getNodesByType
}) => {
  const { createNode, createTypes } = actions

  // Getting google places data

}

// Create pages
exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions

  return graphql(`
    {
      purposes: allPurposesJson {
        edges {
          node {
            slug
          }
        }
      }
      listings: allListings(filter: { id: { ne: "dummy" } }) {
        edges {
          node {
            id
            lang
            city
            slug
            name
            tag
            purpose
            description
            locations
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      Promise.reject(result.errors)
    }

    langs.forEach(lang => {
      // Create purposes pages
      const purposes = result.data.purposes.edges
      purposes.forEach(purpose => {
        const name = t(lang, purpose.node.slug, "purposes")
        const slug = slugify(name)
        const url = `/${t(lang, "purpose")}/${slug}`
        let template = path.resolve(`src/templates/List/List.js`)
        createPage({
          path: url,
          component: template,
          context: {
            purpose: purpose.node
          }
        })
      })
      // Create listings pages
      const listings = result.data.listings.edges.filter(
        listing => listing.node.lang == lang
      )
      listings.forEach(listing => {
        const url = `/${t(lang, "listing")}/${listing.node.slug}`
        let template = path.resolve(`src/templates/Listing/Listing.js`)
        if (listing.node && listing.node.name) {
          createPage({
            path: url,
            component: template,
            context: {
              listing: listing.node,
              slugRegex: `/listings\/${lang}\/${listing.node.slug}/`
            }
          })
        }
      })
    })
  })
}

gatsby-browser.js: N/A

gatsby-ssr.js: N/A

freiksenet commented 5 years ago

Could you please include source for your components too (and if possible a runnable reproduction)?

faldunate commented 1 year ago

Hi @benjam1n , how did you finally solve this problem? We have the same error.