next-i18next Jest Testing with useTranslation
Asked Answered
S

7

22

Testing libs...always fun. I am using next-i18next within my NextJS project. We are using the useTranslation hook with namespaces.

When I run my test there is a warning:

console.warn react-i18next:: You will need to pass in an i18next instance by using initReactI18next

> 33 |   const { t } = useTranslation(['common', 'account']);
     |                 ^

I have tried the setup from the react-i18next test examples without success. I have tried this suggestion too.

as well as just trying to mock useTranslation without success.

Is there a more straightforward solution to avoid this warning? The test passes FWIW...

test('feature displays error', async () => {
    const { findByTestId, findByRole } = render(
      <I18nextProvider i18n={i18n}>
        <InviteCollectEmails onSubmit={jest.fn()} />
      </I18nextProvider>,
      {
        query: {
          orgId: 666,
        },
      }
    );

    const submitBtn = await findByRole('button', {
      name: 'account:organization.invite.copyLink',
    });

    fireEvent.click(submitBtn);

    await findByTestId('loader');

    const alert = await findByRole('alert');
    within(alert).getByText('failed attempt');
  });

Last, is there a way to have the translated plain text be the outcome, instead of the namespaced: account:account:organization.invite.copyLink?

Seeseebeck answered 6/4, 2021 at 17:47 Comment(3)
Did you find the solution?Brainstorming
no...i might look into github.com/vinissimus/next-translateSeeseebeck
How about now? Did you find a solution?Grati
P
17

Use the following snippet before the describe block OR in beforeEach() to mock the needful.

jest.mock("react-i18next", () => ({
    useTranslation: () => ({ t: key => key }),
}));

Hope this helps. Peace.

Premarital answered 25/5, 2021 at 7:52 Comment(3)
For me, it just worked placing it before the describe() blog.Glauconite
Yeah, putting this to "beforeAll" or other hooks wont do the trick, but what the other comment said did it for mePopularity
I put it in my jest.setup.js, also I changed the mock for the t function slightly, to accept params: t: (key, parameters) => (parameters ? key + JSON.stringify(parameters) : key),Deprived
S
3

I figured out how to make the tests work with an instance of i18next using the renderHook function and the useTranslation hook from react-i18next based on the previous answers and some research.

This is the Home component I wanted to test:

import { useTranslation } from 'next-i18next';

const Home = () => {
  const { t } = useTranslation("");
  return (
    <main>
      <div>
        <h1> {t("welcome", {ns: 'home'})}</h1>
      </div>
    </main>
  )
};

export default Home;

First, we need to create a setup file for jest so we can start an i18n instance and import the translations to the configuration. test/setup.ts

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import homeES from '@/public/locales/es/home.json';
import homeEN from '@/public/locales/en/home.json';

i18n.use(initReactI18next).init({
  lng: "es",
  resources: {
    en: {
      home: homeEN,
    },
    es: {
      home: homeES,
    }
  },
  fallbackLng: "es",
  debug: false,
});

export default i18n;

Then we add the setup file to our jest.config.js:

setupFilesAfterEnv: ["<rootDir>/test/setup.ts"]

Now we can try our tests using the I18nextProvider and the useTranslation hook:

import '@testing-library/jest-dom/extend-expect';
import { cleanup, render, renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { I18nextProvider, useTranslation } from 'react-i18next';

import Home from '.';

describe("Index page", (): void => {
  afterEach(cleanup);

  it("should render properly in Spanish", (): void => {
    const t = renderHook(() => useTranslation());

    const component = render( 
      <I18nextProvider i18n={t.result.current.i18n}>
        <Home / >
      </I18nextProvider>
    );

    expect(component.getByText("Bienvenido a Pocky")).toBeInTheDocument();
  });

  it("should render properly in English", (): void => {
    const t = renderHook(() => useTranslation());
    act(() => {
      t.result.current.i18n.changeLanguage("en");
    });

    const component = render( 
      <I18nextProvider i18n={t.result.current.i18n}>
        <Home/>
      </I18nextProvider>
    );
    expect(component.getByText("Welcome to Pocky")).toBeInTheDocument();
  });

});

Here we used the I18nextProvider and send the i18n instance using the useTranslation hook. after that the translations were loaded without problems in the Home component.

We can also change the selected language running the changeLanguage() function and test the other translations.

Sunder answered 17/2, 2023 at 22:24 Comment(0)
D
1

use this for replace render function.


import { render, screen } from '@testing-library/react'
import DarkModeToggleBtn from '../../components/layout/DarkModeToggleBtn'
import { appWithTranslation } from 'next-i18next'
import { NextRouter } from 'next/router'


jest.mock('react-i18next', () => ({
    I18nextProvider: jest.fn(),
    __esmodule: true,
 }))

  
const createProps = (locale = 'en', router: Partial<NextRouter> = {}) => ({
    pageProps: {
        _nextI18Next: {
        initialLocale: locale,
        userConfig: {
            i18n: {
            defaultLocale: 'en',
            locales: ['en', 'fr'],
            },
        },
        },
    } as any,
    router: {
        locale: locale,
        route: '/',
        ...router,
    },
} as any)

const Component = appWithTranslation(() => <DarkModeToggleBtn />)

const defaultRenderProps = createProps()

const renderComponent = (props = defaultRenderProps) => render(
    <Component {...props} />
)


describe('', () => {
    it('', () => {

        renderComponent()
     
        expect(screen.getByRole("button")).toHaveTextContent("")

    })
})


Darkling answered 11/1, 2022 at 19:51 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Erotica
G
0

I used a little bit more sophisticated approach than mocking to ensure all the functions work the same both in testing and production environment.

First, I create a testing environment:

// testing/env.ts
import i18next, { i18n } from "i18next";
import JSDomEnvironment from "jest-environment-jsdom";
import { initReactI18next } from "react-i18next";

declare global {
  var i18nInstance: i18n;
}

export default class extends JSDomEnvironment {
  async setup() {
    await super.setup();
    /* The important part start */
    const i18nInstance = i18next.createInstance();
    await i18nInstance.use(initReactI18next).init({
      lng: "cimode",
      resources: {},
    });
    this.global.i18nInstance = i18nInstance;
    /* The important part end */
  }
}

I add this environment in jest.config.ts:

// jest.config.ts
export default {
  // ...
  testEnvironment: "testing/env.ts",
};

Sample component:

// component.tsx
import { useTranslation } from "next-i18next";

export const Component = () => {
  const { t } = useTranslation();
  return <div>{t('foo')}</div>
}

And later on I use it in tests:

// component.test.tsx
import { setI18n } from "react-i18next";
import { create, act, ReactTestRenderer } from "react-test-renderer";
import { Component } from "./component";

it("renders Component", () => {
  /* The important part start */
  setI18n(global.i18nInstance);
  /* The important part end */
  let root: ReactTestRenderer;
  act(() => {
    root = create(<Component />);
  });
  expect(root.toJSON()).toMatchSnapshot();
});
Godbey answered 17/2, 2023 at 9:46 Comment(0)
C
0

For those who couldn't find a solution or any help in the library itself, this is what I ended up doing to actually load the translations and test them.

I'm using Next JS v12.x and next-i18next v12.1.0 along with jest and testing-library but could be applied to other environments.

  1. I added a testing utility file src/utils/testing.ts
/* eslint-disable import/no-extraneous-dependencies */
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import { DEFAULT_LOCALE } from 'src/utils/constants';

/**
 * Initializes the i18n instance with the given namespaces.
 * @param {string[]} namespaces - An array of namespaces.
 * @param {string} locale - The locale to use.
 * @returns {i18n.i18n} The initialized i18n instance.
 */
const initializeI18n = async (
  namespaces: string[],
  locale = DEFAULT_LOCALE
) => {
  const resources: { [ns: string]: object } = {};

  // Load resources for the default language and given namespaces
  namespaces.forEach((ns) => {
    const filePath = `public/locales/${locale}/${ns}.json`;
    try {
      // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require,import/no-dynamic-require
      const translations = require(`../../${filePath}`);
      resources[ns] = translations;
    } catch (error) {
      throw new Error(
        `Could not load translations for locale: ${locale}, namespace: ${ns}`
      );
    }
  });

  await i18n.use(initReactI18next).init({
    lng: locale,
    fallbackLng: locale,
    debug: false,
    ns: namespaces,
    defaultNS: namespaces[0],
    resources: { [locale]: resources },
    interpolation: { escapeValue: false },
  });

  return i18n;
};

export default initializeI18n;
  1. On my test file I asynchronously initialized the instance with the namespace I wanted to load, rendered my component and awaited for the screen to find the rendered translated text.
describe('when price is zero', () => {
  beforeEach(async () => {
    await initializeI18n(['common_areas']);

    render(<CommonAreaCard commonArea={mockCommonArea(0)} />);
  });

  it('should render the free price', async () => {
    expect(
      await screen.findByText('Sin costo de reservación')
    ).toBeInTheDocument();
  });
});

I hope it helps.

Chophouse answered 29/8, 2023 at 19:25 Comment(0)
H
0

I got it working by doing the following: Add this to your package.json

"jest": {
  "setupFiles": [
    "./jestSetupFile.js"
  ]
}

Then add this in jestSetupFile.js

jest.mock("react-i18next", () => ({
  useTranslation: () => {
    return {
      t: (str) => str,
    };
  },
}));
Hildegard answered 13/9, 2023 at 3:48 Comment(0)
T
0

Regarding your last question, I found a way to convert namespace to its actual translated value. For some reason wrapping the provider never worked for me hence I had to look into this new alternative.

The original mock by the documentation:

jest.mock('react-i18next', () => ({
 
  useTranslation: () => {
    return {
      t: (str) => str,
      i18n: {
        changeLanguage: () => new Promise(() => {}),
      },
    };
  },
  initReactI18next: {
    type: '3rdParty',
    init: () => {},
  }
}));

We'll need to somehow convert t(str) => str to return the translated value rather than the namespace. Take note it returns a string. So we have to somehow convert, for example, "button.proceed" to englishFile["button"]["proceed"]. By doing this, it will return the translated value.

This function does just that.

const translate = (givenString, file) => {

const splitArr = givenString.split(".");
let result = file;
for (let i = 0; i < splitArr.length; i++) {
    result = result[splitArr[i]];
}
    return result;
};

And now, the new mock should look something like this,

jest.mock("react-i18next", () => ({
    useTranslation: () => {
        // translation files should be placed here to avoid error warnings.
        const enFile = jest.requireActual("../../locales/en.json");
        return {
            t: (key) => translate(key, enFile),
            i18n: {
                changeLanguage: () => new Promise(() => {}),
            },
        };
    },
    initReactI18next: {
        type: "3rdParty",
        init: () => {},
    },
    I18nextProvider: ({ children }) => children,
}));
Toolmaker answered 25/9, 2023 at 8:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.