Repository: aralroca/next-translate (2026-03-22T06:21:20Z)

Issue:
I18nProvider ignores lang after upgrade from 3.0.0 to 3.0.2
After upgrading next-translate from 3.0.0 to 3.0.2 I can no longer change my app's language. Regardless of which language I pass to I18nProvider, the app always renders using the default language. Rolling back to 3.0.0 restores expected behavior.

Environment:
next: 16.1.6 (webpack)
react: 19.2.4
next-translate-plugin: 2.6.2 (also tried 3.0.2)
next-translate: 3.0.2 (next-translate-plugin peer dependecy)

Reproduction:

```ts
export default function App({ Component, pageProps }: AppProps) {
  const [lang, setLang] = useState('en')

  return (
    <I18nProvider key={lang} lang={lang}>
      <DynamicNamespaces namespaces={['common']}>
        <Component {...pageProps} onLanguageChange={setLang} />
      </DynamicNamespaces>
    </I18nProvider>
  )
}
```

```ts
export default function Home({ onLanguageChange }: HomeProps) {
  const { t, lang } = useTranslation('common')
  return (
    <div className="h-svh flex flex-col items-center justify-center">
      <LanguageSelector onLanguageChange={onLanguageChange} />
      <strong className="mt-4 text-2xl">Current Language:</strong>{' '}
      {t('languageName')} ({lang})
    </div>
  )
}
```
```ts
export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
  onLanguageChange,
}) => {
  const { t, lang } = useTranslation('common')

  const LANGUAGES = [
  { code: 'en', name: 'English' },
  { code: 'es', name: 'Español' },
  { code: 'fr', name: 'Français' },
  { code: 'pt', name: 'Português' },
]

  const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newLang = e.target.value
    if (onLanguageChange) {
      onLanguageChange(newLang)
    }
  }

  return (
    <div>
      <label htmlFor="language-select">
        {t('selectLabel')}
      </label>
      <select
        id="language-select"
        value={lang}
        onChange={handleLanguageChange}
      >
        {LANGUAGES.map((language) => (
          <option key={language.code} value={language.code}>
            {language.name}
          </option>
        ))}
      </select>
    </div>
  )
}
```
Or clone this project: https://github.com/dmg-vicente/next-language-bug (uses 3.0.0 by default)



Discussion:
Please @dmg-vicente try version 3.1.0-canary.2, as we made a similar fix there. If it doesn't work, let us know and we'll investigate further.
Thanks for the quick response. Just tried it, but sadly the problem still persists
Are you using the Pages Router or the App Router? If possible, could you provide a repository with a reproducible example? 🙏
I'm using pages router, and yes, here: https://github.com/dmg-vicente/next-language-bug
@dmg-vicente fixed in https://github.com/aralroca/next-translate/releases/tag/3.1.0-canary.5 It should work well now. Please, any feedback is welcome.
Thank you very much, I'll give it a try as soon as I can
I did some tests on my end and it looks like the issue still occurs when using webpack, but it's working correctly with turbopack



Patch:
diff --git a/src/DynamicNamespaces.tsx b/src/DynamicNamespaces.tsx
index ce9c1e4..520c5f3 100644
--- a/src/DynamicNamespaces.tsx
+++ b/src/DynamicNamespaces.tsx
@@ -16,19 +16,28 @@ export default function DynamicNamespaces({
   const loadLocale =
     dynamic || config.loadLocaleFrom || (() => Promise.resolve({}))
 
-  async function loadNamespaces() {
-    if (typeof loadLocale !== 'function') return
+  useEffect(() => {
+    let isCancelled = false
 
-    const pageNamespaces = await Promise.all(
-      namespaces.map((ns) => loadLocale(lang, ns))
-    )
-    setPageNs(pageNamespaces)
-    setLoaded(true)
-  }
+    async function loadNamespaces() {
+      if (typeof loadLocale !== 'function') return
+
+      const pageNamespaces = await Promise.all(
+        namespaces.map((ns) => loadLocale(lang, ns))
+      )
+
+      if (isCancelled) return
+
+      setPageNs(pageNamespaces)
+      setLoaded(true)
+    }
 
-  useEffect(() => {
     loadNamespaces()
-  }, [namespaces.join()])
+
+    return () => {
+      isCancelled = true
+    }
+  }, [namespaces.join(), lang])
 
   if (!loaded) return fallback || null
 
diff --git a/src/I18nProvider.tsx b/src/I18nProvider.tsx
index 8230eb3..001d05d 100644
--- a/src/I18nProvider.tsx
+++ b/src/I18nProvider.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useContext } from 'react'
+import React, { createContext, useContext, useMemo } from 'react'
 import { useRouter } from 'next/router'
 import I18nContext from './context'
 import transCore from './transCore'
@@ -25,8 +25,14 @@ export default function I18nProvider({
   const config = { ...internal.config, ...newConfig }
   const localesToIgnore = config.localesToIgnore || ['default']
   const ignoreLang = !lang || localesToIgnore.includes(lang)
-  const pluralRules = new Intl.PluralRules(ignoreLang ? undefined : lang)
-  const t = transCore({ config, allNamespaces, pluralRules, lang })
+  const pluralRules = useMemo(
+    () => new Intl.PluralRules(ignoreLang ? undefined : lang),
+    [ignoreLang, lang]
+  )
+  const t = useMemo(
+    () => transCore({ config, allNamespaces, pluralRules, lang }),
+    [config, allNamespaces, pluralRules, lang]
+  )
 
   return (
     <I18nContext.Provider value={{ lang, t }}>


Test patch:
diff --git a/__tests__/DynamicNamespaces.test.js b/__tests__/DynamicNamespaces.test.js
new file mode 100644
index 0000000..e9e0484
--- /dev/null
+++ b/__tests__/DynamicNamespaces.test.js
@@ -0,0 +1,120 @@
+import React, { useState } from 'react'
+import { render, cleanup, fireEvent, act } from '@testing-library/react'
+import I18nProvider from '../src/I18nProvider'
+import useTranslation from '../src/useTranslation'
+import DynamicNamespaces from '../src/DynamicNamespaces'
+
+describe('DynamicNamespaces', () => {
+  afterEach(cleanup)
+
+  test('should reload namespaces when lang changes', async () => {
+    const loadLocale = jest.fn((lang, ns) => {
+      const locales = {
+        en: { common: { hello: 'Hello' } },
+        es: { common: { hello: 'Hola' } },
+      }
+      return Promise.resolve(locales[lang][ns])
+    })
+
+    const App = () => {
+      const [lang, setLang] = useState('en')
+      return (
+        <I18nProvider lang={lang} config={{ loadLocaleFrom: loadLocale }}>
+          <DynamicNamespaces namespaces={['common']}>
+            <Content />
+          </DynamicNamespaces>
+          <button onClick={() => setLang('es')}>Change to ES</button>
+        </I18nProvider>
+      )
+    }
+
+    const Content = () => {
+      const { t } = useTranslation('common')
+      return <span data-testid="text">{t('hello')}</span>
+    }
+
+    const { getByTestId, getByText } = render(<App />)
+
+    await act(async () => {
+      await Promise.resolve()
+    })
+
+    expect(getByTestId('text').textContent).toBe('Hello')
+    expect(loadLocale).toHaveBeenCalledWith('en', 'common')
+
+    await act(async () => {
+      fireEvent.click(getByText('Change to ES'))
+    })
+
+    expect(loadLocale).toHaveBeenCalledWith('es', 'common')
+
+    await act(async () => {
+      await Promise.resolve()
+    })
+
+    expect(getByTestId('text').textContent).toBe('Hola')
+  })
+
+  test('should not suffer from race conditions when lang changes rapidly', async () => {
+    let resolveES
+    const esPromise = new Promise((resolve) => {
+      resolveES = () => resolve({ hello: 'Hola' })
+    })
+
+    const loadLocale = jest.fn((lang, ns) => {
+      if (lang === 'en') return Promise.resolve({ hello: 'Hello' })
+      if (lang === 'es') return esPromise
+      return Promise.resolve({})
+    })
+
+    const App = () => {
+      const [lang, setLang] = useState('en')
+      return (
+        <I18nProvider lang={lang} config={{ loadLocaleFrom: loadLocale }}>
+          <DynamicNamespaces namespaces={['common']}>
+            <Content />
+          </DynamicNamespaces>
+          <button onClick={() => setLang('es')}>Change to ES</button>
+          <button onClick={() => setLang('en')}>Change back to EN</button>
+        </I18nProvider>
+      )
+    }
+
+    const Content = () => {
+      const { t } = useTranslation('common')
+      return <span data-testid="text">{t('hello')}</span>
+    }
+
+    const { getByTestId, getByText } = render(<App />)
+
+    // Initial load EN
+    await act(async () => {
+      await Promise.resolve()
+    })
+    expect(getByTestId('text').textContent).toBe('Hello')
+
+    // Change to ES (will be pending)
+    await act(async () => {
+      fireEvent.click(getByText('Change to ES'))
+    })
+
+    // Change back to EN immediately
+    await act(async () => {
+      fireEvent.click(getByText('Change back to EN'))
+    })
+
+    // Wait for EN to resolve (it resolves instantly in our mock)
+    await act(async () => {
+      await Promise.resolve()
+    })
+
+    // Now resolve the stale ES request
+    await act(async () => {
+      resolveES()
+      await esPromise
+    })
+
+    // It should still be 'Hello' because the ES request was stale
+    expect(getByTestId('text').textContent).toBe('Hello')
+  })
+})
diff --git a/__tests__/I18nProvider.test.js b/__tests__/I18nProvider.test.js
new file mode 100644
index 0000000..2ba73b9
--- /dev/null
+++ b/__tests__/I18nProvider.test.js
@@ -0,0 +1,47 @@
+import React, { useState } from 'react'
+import { render, cleanup, fireEvent, act } from '@testing-library/react'
+import I18nProvider from '../src/I18nProvider.tsx'
+import useTranslation from '../src/useTranslation.tsx'
+
+describe('I18nProvider', () => {
+  afterEach(cleanup)
+
+  test('should update translation function when lang changes', async () => {
+    const App = () => {
+      const [lang, setLang] = useState('en')
+      const namespaces =
+        lang === 'en'
+          ? { common: { hello: 'Hello' } }
+          : { common: { hello: 'Hola' } }
+
+      return (
+        <I18nProvider lang={lang} namespaces={namespaces}>
+          <Content />
+          <button onClick={() => setLang('es')}>Change to ES</button>
+        </I18nProvider>
+      )
+    }
+
+    const Content = () => {
+      const { t, lang } = useTranslation('common')
+      return (
+        <div>
+          <span data-testid="lang">{lang}</span>
+          <span data-testid="text">{t('hello')}</span>
+        </div>
+      )
+    }
+
+    const { getByTestId, getByText } = render(<App />)
+
+    expect(getByTestId('lang').textContent).toBe('en')
+    expect(getByTestId('text').textContent).toBe('Hello')
+
+    await act(async () => {
+      fireEvent.click(getByText('Change to ES'))
+    })
+
+    expect(getByTestId('lang').textContent).toBe('es')
+    expect(getByTestId('text').textContent).toBe('Hola')
+  })
+})