Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example of HMR with next-i18next@8 and next@10 #32

Closed
skrivanos opened this issue Mar 13, 2021 · 32 comments
Closed

Example of HMR with next-i18next@8 and next@10 #32

skrivanos opened this issue Mar 13, 2021 · 32 comments

Comments

@skrivanos
Copy link

The API of next-i18next changes quite drastically with version 8 (Next v10 support). I'm not sure if this lib is still compatible (or if you want it to be), but if so it would be great with an example of how to get it going.

@felixmosh
Copy link
Owner

Yeap I'm aware of the changes, for now there is no way to get a reference to the i18next instance (that I'm aware of).
It was discussed in here i18next/next-i18next#983 (comment)

@skrivanos
Copy link
Author

skrivanos commented Mar 13, 2021

Oh, I didn't realise you're the author of this lib (I actually created the linked issue). The quick fix has been merged, i.e. next-i18next now exports i18n that is either an instance or null. However, I'm not sure if it's applicable to this lib since it's only possible to get it after App has been rendered.

@felixmosh
Copy link
Owner

Oh, I didn't realise you're the author of this lib (I actually created the linked issue). The quick fix has been merged, i.e. next-i18next now exports i18n that is either an instance or null. However, I'm not sure if it's applicable to this lib since it's only possible to get it after App has been rendered.

It doesn't matter where to put the hmr, it just need to get the instance.

@felixmosh
Copy link
Owner

I've tried next-i18next v8, if you add

// _app.js

import { appWithTranslation, i18n } from 'next-i18next';

if (process.env.NODE_ENV === 'development') {
  if (typeof window === 'undefined') {
    const { applyServerHMR } = require('i18next-hmr/server');
    applyServerHMR(i18n);
  } else {
    const { applyClientHMR } = require('i18next-hmr/client');
    applyClientHMR(i18n);
  }
}

const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;

export default appWithTranslation(MyApp);

It works, but it reloads the page when locale are changed.
Since the new version doesn't load the locale on client side at all, (it doesn't use the i18next-xhr-backend at all), my lib won't be able to refetch the new json.

@skrivanos
Copy link
Author

Thanks. I tried and couldn't get it to work that way since the i18n instance is null at that point (appWithTranslation has not been run yet). Also, the instance is re-created every time the route or locale changes. I had to do something really ugly like this:

import { appWithTranslation, i18n } from 'next-i18next'
import i18NextConfig from '../next-i18next.config'

let _lastInstance = null

if (process.env.NODE_ENV === 'development') {
  setInterval(() => {
    if (i18n && _lastInstance !== i18n) {
      if (typeof window === 'undefined') {
        const { applyServerHMR } = require('i18next-hmr/server')
        applyServerHMR(i18n)
      } else {
        const { applyClientHMR } = require('i18next-hmr/client')
        applyClientHMR(i18n)
      }

      _lastInstance = i18n
    }
  }, 1000)
}

const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />

export default appWithTranslation(MyApp, i18NextConfig)

Regarding the reloading, I'm pretty sure the i18next-http-backend is going to have to be included by default soon. See i18next/next-i18next#1049. In the meantime, it can be solved like so:

const HttpBackend = require('i18next-http-backend').default

module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de'],
  },
  serializeConfig: false,  
  backend: process.browser ? {
    loadPath: '/locales/{{lng}}/{{ns}}.json', // this shouldn't be needed, but there's a bug with stripping /public in next-i18next
  } : undefined,
  use: process.browser ? [HttpBackend] : [],
}

@felixmosh
Copy link
Owner

I see, unfortunately as it was discussed in here, currently, next-i18next creating new instance on each page nav.

I don't have anything to do (as this lib author) in order to "overcome" it.
When it will add the i18next-http-backend plugin it should work...

I'm open for discussion of how to solve these issues. (In my project I fixed the version on v7)

@stomvi
Copy link

stomvi commented Apr 3, 2021

Facing the same problem here. I think it is more likely the next-i18next has to provide a way to decouple the creation of the i18n instance.

Great job.

@wagerfield
Copy link

wagerfield commented Jun 9, 2021

For anyone else attempting to do this, I arrived at a pretty elegant solution 🎉

  1. Install these two dependencies:
yarn add i18next-hmr i18next-http-backend --dev
  1. Update the next.config.js like so:
const { resolve } = require("path")
const { I18NextHMRPlugin } = require("i18next-hmr/plugin")

const localesDir = resolve("public/locales")

module.exports = {
  // ...other next config options
  webpack(config, context) {
    if (!context.isServer && context.dev) {
      config.plugins.push(new I18NextHMRPlugin({ localesDir }))
    }

    return config
  },
}
  1. Update the next-i18next.config.js like so:
const HttpBackend = require("i18next-http-backend/cjs")

module.exports = {
  // ...other next-i18next config options
  use: process.browser ? [HttpBackend] : [],
}
  1. Create a hook for applying the HMR code during development. I created mine in hooks/use-hmr.ts and it looks like this:
import { useTranslation } from "next-i18next"
import { useEffect } from "react"

export const useHMR = (): void => {
  const { i18n } = useTranslation()

  if (process.env.NODE_ENV === "development" && !process.browser) {
    import("i18next-hmr/server").then(({ applyServerHMR }) => {
      applyServerHMR(i18n)
    })
  }

  useEffect(() => {
    if (process.env.NODE_ENV === "development") {
      import("i18next-hmr/client").then(({ applyClientHMR }) => {
        applyClientHMR(i18n)
      })
    }
  }, [i18n])
}
  1. Import and pass next-i18next.config.js to appWithTranslation (typically in pages/_app.tsx) and use the HMR hook created above within the body of the App component:
import { AppProps } from "next/app"
import { FunctionComponent } from "react"
import { appWithTranslation } from "next-i18next"
import { useHMR } from "../hooks/use-hmr"
import i18nextConfig from "../../next-i18next.config"

export const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => {
  useHMR()

  return <Component {...pageProps} />
}

export default appWithTranslation(App, i18nextConfig) // Required to use the HttpBackend that enables HMR on the client

The beauty in this solution is the useHMR hook since it will re-apply the HMR code whenever the i18n instance changes on the client. It also nicely encapsulates the logic for applying the HMR code on the server and client.

@felixmosh perhaps this is something that the repo should export from a nested react module eg.

import { useHMR } from "i18next-hmr/react"

Wrapping the 2 dynamic imports for the client and server "apply HMR" functions with the process.env checks means that the code is stripped out during production builds.

Happy HMR'in! 🕺

@wagerfield
Copy link

@felixmosh the only issue that I've just noticed with my example above is that the client HMR code doesn't seem to be picking up the changes to non-default namespace files?

In my project I have several namespaces, but only changes to the defaultNS JSON files are picked up on the client.

On the server all namespaces are picked up, so when I refresh the page I see the latest changes.

Any ideas? Is it something to do with the HttpBackend that's used on the client? Am I missing some configuration?

@felixmosh
Copy link
Owner

@wagerfield thank you for sharing your idea 🙏🏼

i18next-hmr triggers a usage of http-backend on the client side, this means that you must configure one.

next-i18next v8 dropped it in favor of getServerProps (this become the translation fetcher) so maybe it doesn't configure all namespaces.

Can you prepare a demo repo so I will poke around?

@wagerfield
Copy link

@felixmosh here we are kind Sir! https://github.com/wagerfield/i18next-hmr-hook

If you start the dev server with yarn dev and then try editing any of the core.json translation files (the defaultNS as specified in next-i18next.config.js) ...HMR works both on the client and server.

You get the same lovely logs in both environments:

[I18NextHMR] Got an update with en-US/core
[I18NextHMR] Update applied successfully

...and Next does a Fast Refresh and everything works as expected. Hurray 🎉

However if you edit any of the page.json translation files (not the defaultNS) then the HMR logs only show on the server and the changes don't update on the client via HMR. You have to refresh the page to see them.

As mentioned above, the only obvious difference to me is the use of the HttpBackend on the client which is added as a plugin in next-i18next.config.js.

Any help would be much appreciated.

@felixmosh
Copy link
Owner

From what I've observed, looks like the HMR is working, You do get an update on the page namespace.
image
But from the config, looks like i18next knows only the core NS.
image

I've found the issue, it is here.

image
Next-I18next sets the ns to contain only the default namespace therefore i18next-hmr doesn't recognize it as a valid namespace :]

@wagerfield
Copy link

@felixmosh that is some excellent debugging—thank you! Commenting out that line resolves the issue.

I'm curious as to why the ns is being overridden here—I'll open an issue with next-i18next now and reference this thread.

Thanks again 🙌

@wagerfield
Copy link

@felixmosh (and anyone else following this thread) the PR that I put in to resolve the client side ns bug in next-i18next that @felixmosh identified is now merged and published to npm ([email protected]) 🎉

I can confirm that both client and server side HMR works with the useHMR hook approach I shared above and can be seen in this demo repo:

https://github.com/wagerfield/i18next-hmr-hook

@felixmosh with this PoC, do you think this hook should make its way into your repo under a i18next-hmr/react module? Happy to put in a PR if so.

@felixmosh
Copy link
Owner

felixmosh commented Jun 14, 2021

Glad to hear.
I don't think that i18next-hmr should be a framework specific.

@wagerfield maybe ot can be as part of examples folder

@VityaSchel
Copy link

For anyone else attempting to do this, I arrived at a pretty elegant solution 🎉

  1. Install these two dependencies:
yarn add i18next-hmr i18next-http-backend --dev
  1. Update the next.config.js like so:
const { resolve } = require("path")
const { I18NextHMRPlugin } = require("i18next-hmr/plugin")

const localesDir = resolve("public/locales")

module.exports = {
  // ...other next config options
  webpack(config, context) {
    if (!context.isServer && context.dev) {
      config.plugins.push(new I18NextHMRPlugin({ localesDir }))
    }

    return config
  },
}
  1. Update the next-i18next.config.js like so:
const HttpBackend = require("i18next-http-backend/cjs")

module.exports = {
  // ...other next-i18next config options
  use: process.browser ? [HttpBackend] : [],
}
  1. Create a hook for applying the HMR code during development. I created mine in hooks/use-hmr.ts and it looks like this:
import { useTranslation } from "next-i18next"
import { useEffect } from "react"

export const useHMR = (): void => {
  const { i18n } = useTranslation()

  if (process.env.NODE_ENV === "development" && !process.browser) {
    import("i18next-hmr/server").then(({ applyServerHMR }) => {
      applyServerHMR(i18n)
    })
  }

  useEffect(() => {
    if (process.env.NODE_ENV === "development") {
      import("i18next-hmr/client").then(({ applyClientHMR }) => {
        applyClientHMR(i18n)
      })
    }
  }, [i18n])
}
  1. Import and pass next-i18next.config.js to appWithTranslation (typically in pages/_app.tsx) and use the HMR hook created above within the body of the App component:
import { AppProps } from "next/app"
import { FunctionComponent } from "react"
import { appWithTranslation } from "next-i18next"
import { useHMR } from "../hooks/use-hmr"
import i18nextConfig from "../../next-i18next.config"

export const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => {
  useHMR()

  return <Component {...pageProps} />
}

export default appWithTranslation(App, i18nextConfig) // Required to use the HttpBackend that enables HMR on the client

The beauty in this solution is the useHMR hook since it will re-apply the HMR code whenever the i18n instance changes on the client. It also nicely encapsulates the logic for applying the HMR code on the server and client.

@felixmosh perhaps this is something that the repo should export from a nested react module eg.

import { useHMR } from "i18next-hmr/react"

Wrapping the 2 dynamic imports for the client and server "apply HMR" functions with the process.env checks means that the code is stripped out during production builds.

Happy HMR'in! 🕺

Hello, I've encountered a problem while using your HMR server: when I open / route, I see Error: Initial locale argument was not passed into serverSideTranslations error, and when I open any of locales route like /en, it throws 404. I have followed all your steps. Can you help me?

@KingMatrix1989
Copy link

KingMatrix1989 commented May 21, 2022

useHMR hook approach not working for pages with getStaticProps.

@KingMatrix1989
Copy link

KingMatrix1989 commented May 22, 2022

In my case, ns option of next-i18next.config contains only common namespace and in any SSG pages, required namespaces adds with serverSideTranslations function. In this case, i18n.options.ns contains only common, so client-hmr ignore any changes on other JSON files, e.g. faq.json.

I we change source code client-hmr.js as below, problem will be solved:

// other code

    module.hot.accept('./trigger.js', () => {
      const { changedFiles } = require('./trigger.js');
      const currentNSList = Object.keys(i18n.store.data[i18n.language]); // get all loaded namespaces

      const list = changedFiles
        .map((changedFile) => extractLangAndNS(changedFile, currentNSList))
        .filter(({ lang, ns }) => Boolean(lang) && Boolean(ns));

      if (!list.length) {
        return;
      }

      log(`Got an update with ${printList(list)}`);

      return reloadTranslations(list);
    });

#108

@felixmosh
Copy link
Owner

felixmosh commented May 22, 2022

@skmohammadi can you open a separate issue? It doesn't related to this issue.
In the new issue, please provide a way to reproduce the issue.

@KingMatrix1989
Copy link

@felixmosh
Copy link
Owner

Checkout the new example of next-i18next

@Chill-Studio
Copy link

Chill-Studio commented Jul 21, 2022

Checkout the new example of next-i18next

Hello ! I am using next v12.2.2

By doing the same as in next-i18next I have the HMR working ! But when I refesh the page the new translation does not persist and it fallback to the previous one

@felixmosh
Copy link
Owner

Hi @Chill-Studio, do you have a console message on the server console when you are changing translations?

@Chill-Studio
Copy link

Hi @Chill-Studio, do you have a console message on the server console when you are changing translations?

Just that it compiled successfully

See

@felixmosh
Copy link
Owner

Try to delete .next folder and start again.

@Chill-Studio
Copy link

Try to delete .next folder and start again.

The problem persist after deleting .next folder.

Do you want a project to repro ?

@Chill-Studio
Copy link

Chill-Studio commented Jul 21, 2022

Here is the repro

https://github.com/Chill-Studio/repro.git

To start

yarn && yarn dev

@geoapostolidis
Copy link

why on reload window the translation not updated? it updates only on hmr update

@felixmosh
Copy link
Owner

Hi @geoapostolidis this thread has many responses, please open a new issue that describes your issue.

As a first aid, it sounds that your server side HMR is not working, I need more details in order to help you. (Preferable reproduction repo)

@geoapostolidis
Copy link

the hmr wotking well when i update the json file it updates the client correctly but when i refresh the page the translation goes back to the translation file i had when runned npm run dev and if i stop the server and re-run it it loads the updated file

@felixmosh
Copy link
Owner

@geoapostolidis #32 (comment) 👈🏼

@geoapostolidis
Copy link

#137

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants