i18next / react-i18next

Internationalization for react done right. Using the i18next i18n ecosystem.
https://react.i18next.com
MIT License
9.21k stars 1.02k forks source link

language is not set during tests with vitest #1724

Open phun-ky opened 6 months ago

phun-ky commented 6 months ago

🐛 Bug Report

[!NOTE]
I do not think this is a bug, more of a configuration issue/documentation issue, because clearly, I am doing something wrong

Language is not set during tests with vitest. In a dependency I import, this code exists:


function useI18N() {
  var _useTranslation = reactI18next.useTranslation(),
      t = _useTranslation.t,
      language = _useTranslation.i18n.language;

  if (language !== 'nb' && language !== 'en') {
    throw Error('Language must be either nb or en.');
  }

  return t;
}

function withI18n(Component) {
  return function WrappedComponent(props) {
    var t = useI18N();
    return /*#__PURE__*/React__default["default"].createElement(Component, _extends({}, props, {
      i18n: t
    }));
  };
}

This code throws throw Error('Language must be either nb or en.'); when testing using vitest. It does not throw for building with vite nor with the previous test runner jest (with practically same config).

That code is wrapping a component that is consumed by several components up until the file I am testing, which is using this:


export const renderWithI18NContext = (
  component: ReactNode,
  organisation?: Organisation,
  locale = 'nb'
) => {
  return render(componentWithI18NContext(component, organisation, locale));
};

const componentWithI18NContext = (
  …
) => {
  store.dispatch(…);

  i18n.changeLanguage(locale);

  return (
    <Provider store={store}>
      {/* @ts-ignore */}
      <MemoryRouter>
        <SWRConfig value={{ dedupingInterval: 0 }}>
          <Some.Provider value={something}>
            {component}
          </Some.Provider>
        </SWRConfig>
      </MemoryRouter>
    </Provider>
  );
};

With an import i18n that looks like this:

use(initReactI18next).init({
  lng: 'nb',
  fallbackLng: 'nb',
  resources: {
    nb: {
      translation: {
        …
      }
    },
    en: {
      translation: {
        …
      }
    }
  },
  interpolation: {
    escapeValue: false // not needed for react as it escapes by default
  }
});

export default i18n;

Which is consumed like this:

it('should pass', async () => {
  renderWithI18NContext(
    <ComponentTestContainer>
      …
      component={<ComponentToTest {...props} />}
    />
  );

  expect(…);
});

I've narrowed it down to react-i18next is not picking up language, i.e., that use(initReactI18next).init({…}) is not called, or something..

To Reproduce

I cannot produce a reproduction case due to the complexity of the internal (non public) dependencies, which I also think has something to do with this. As stated, I think this is a misconfiguration on my part, not a bug itself.

Expected behavior

That language is set.

Your Environment

adrai commented 6 months ago

make sure the file that contains the i18next.init call is included/required during your tests

phun-ky commented 6 months ago

@adrai thanks for the suggestion. I've tried that with no avail sadly :/ Tried to include it in the test, in the test setup file, the renderWithI18NContext wrapper and in the component I am testing.

adrai commented 6 months ago

hard to help, sorry

geritol commented 3 months ago

@phun-ky this might be due to something called "dual package hazard" https://github.com/vitest-dev/vitest/issues/3287#issuecomment-1534159966

Had a similar issue (main code was using different build of the package than the library adding the following to vite.config.js solved the issue for me:


resolve: {
  alias: {
     'react-i18next': path.resolve(__dirname, './node_modules/react-i18next/dist/commonjs/index.js'), // https://github.com/vitest-dev/vitest/issues/3287#issuecomment-1534159966
  },
},
RomRom1 commented 2 months ago

Hi @phun-ky ,

I had the same problem and I solved it by using i18next-fs-backend in my component wrapper. Also, to make it works, i18n instance needs to be created and initialized synchronously. Please note the await on init call and the option initImmediate: false.

import {render as rtlRender} from '@testing-library/react'
import {I18nextProvider, initReactI18next} from "react-i18next";
import {createInstance} from "i18next";
import Backend from "i18next-fs-backend";
import translation from "../public/locales/en/translation.json"

const instance = createInstance()
await instance
    .use(Backend)
    .use(initReactI18next)
    .init({
        fallbackLng: "en",
        debug: false,
        interpolation: {
            escapeValue: false
        },
        defaultNS: "translation",
        initImmediate: false,
        resources: {
            en: {
                translation
            }
        }
    });

export const render = (ui: JSX.Element) => {

    function Wrapper({children}: any): any {

        return (
            <I18nextProvider i18n={instance}>
                {children}
            </I18nextProvider>
        )
    }

    rtlRender(ui, {wrapper: Wrapper})
}

Hope that can help

tomtom94 commented 4 days ago

Hi there,

I just found an alternative for me, which is pretty well working.

In my case I need to use the var env import.meta.env.MODE === 'test'

import frAdmin from '../public/locales/fr/admin.json'
import frErrorBoundary from '../public/locales/fr/errorBoundary.json'
import frMenu from '../public/locales/fr/menu.json'
import frNewStudy from '../public/locales/fr/studyResults.json'
import frStudyResults from '../public/locales/fr/studyResults.json'

import enAdmin from '../public/locales/en/admin.json'
import enErrorBoundary from '../public/locales/en/errorBoundary.json'
import enMenu from '../public/locales/en/menu.json'
import enNewStudy from '../public/locales/en/studyResults.json'
import enStudyResults from '../public/locales/en/studyResults.json'

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: ['fr'],
    ...(import.meta.env.MODE === 'test' && {
      resources: {
        fr: { admin: frAdmin, errorBoundary: frErrorBoundary, menu: frMenu, newStudy: frNewStudy, studyResults: frStudyResults },
        en: { admin: enAdmin, errorBoundary: enErrorBoundary, menu: enMenu, newStudy: enNewStudy, studyResults: enStudyResults }
      }
    }),
    interpolation: {
      escapeValue: false // not needed for react as it escapes by default
    },
    ...(import.meta.env.MODE !== 'test' && { ns: ['menu', 'studyResults', 'newStudy', 'admin'] })
  })
import { render, screen } from '@testing-library/react'
import { MyComponent } from './MyComponent'
import { I18nextProvider } from 'react-i18next'
import { setupStore } from '../../modules/store'
import { Provider } from 'react-redux'
import i18n from '../../i18n'

const store = setupStore()

test("renders all the stuff you want", () => {
  render(
    <Provider store={store}>
      <I18nextProvider i18n={i18n}>
        <MyComponent />
      </I18nextProvider>
    </Provider>
  )

  expect(screen.getByText('I am looking for my sentence here')).toBeDefined()
})

Actually by just defining the namespaces, with this line ns: ['menu', 'studyResults', 'newStudy', 'admin'] the testing library didn't open the json files, so I had to import them one by one and put them in the classical resources: { fr:{}, en:{} } style attributes.

I have been trying your asynchronous stuff @RomRom1 but not working.