i18next / react-i18next

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

Unable to have multiple i18next instances (React apps) on the same page #726

Closed msheakoski closed 5 years ago

msheakoski commented 5 years ago

I have a page which contains 2 React apps, each having their own i18n.js config file like the one below. The last app to load seems to conflict with the "translation" namespace of the first app and triggers an error. The only way that I can have both of them load successfully is to import the i18n.js from the first app and add additional namespaces to it at runtime. However, doing it this way creates a dependency between two unrelated apps.

// React App #1
// project/src/ReactApp1/i18n.js

i18n
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
        fallbackLng: "en",
        interpolation: {escapeValue: false},
        resources: {
            "en": {translation: require("./translations/en.json")},
        },
    });

export default i18n;
// Workaround for React App #2
// project/src/ReactApp2/i18n.js

// Ideally I would like to use a config file like the one above instead of this one which
// depends on the first config.
import i18n from "../ReactApp1/i18n";

i18n.addResourceBundle("en", "a_different_namespace", require("./translations/en.json"));

export default i18n;

Occurs in react-i18next version i18next 14.1.1 / react-i18next 10.0.2

Expected behaviour I expected that two separate i18next instances would not overwrite each other's translation namespaces.

jamuhl commented 5 years ago

So you're using the same i18next instance on both apps?

create separate instances like: https://www.i18next.com/overview/api#createinstance

msheakoski commented 5 years ago

I have created two separate instances as you suggested in your last comment, but the second instance still contains translations from the namespace of the first instance:

Instance 1

import * as LanguageDetector from "i18next-browser-languagedetector";
import i18n from "i18next";
import {initReactI18next} from "react-i18next";

const newInstance1 = i18n.createInstance();

newInstance1
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
        fallbackLng: "en",
        interpolation: {
            escapeValue: false,
        },
        resources: {
            "en": {translation: require("./translations/en.json")},
            "en-GB": {translation: require("./translations/en-GB.json")},
        },
    });

export default newInstance1;

Instance 2

import * as LanguageDetector from "i18next-browser-languagedetector";
import i18n from "i18next";
import {initReactI18next} from "react-i18next";

const newInstance2 = i18n.createInstance();

newInstance2
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
        fallbackLng: "en",
        interpolation: {
            escapeValue: false,
        },
        resources: {
            "en": {translation: require("./translations/en.json")},
        },
    });

export default newInstance2;

When I log the i18n instance in the second app via const {t, i18n} = this.props; console.log(i18n); i18n.store.data contains translations from the first instance.

jamuhl commented 5 years ago

Ah and further your using same react-i18next for both apps? If so .use(initReactI18next) won't work as it's one global state. You will need to use the I18nextProvider: https://react.i18next.com/latest/i18nextprovider#what-it-does to pass i18n to your JSX tree

msheakoski commented 5 years ago

Thank you for your advice, @jamuhl! I will give this a try in the next day and report my results. Does the <I18nextProvider> tag need to be outside of my <App> component, or can I put it as the outermost tag inside of my 's render method like this?

class App {
  render() {
    return (
      <I18nextProvider i18n={i18n}>
        <div>Hello, world!</div>
        <OtherComponent/>
      </I18nextProvider>
    );
  }
}
jamuhl commented 5 years ago

Just remove the .use(initReactI18next) in i18n.js and place the I18nextProvider where you like -> just must be a parent of all the other provided react-i18next components so they can pick up the i18next instance passed in from context

msheakoski commented 5 years ago

It worked! I left an example for any future readers:

Edit j27n3k65yw

I appreciate your assistance with my issue and also the great work you have put into i18next!

ferrarisMat commented 3 years ago

@msheakoski thanks, unfortunately, your CSB link leads to a 404

ItayTur commented 3 years ago

hey @jamuhl , i tried the solution suggested but it didn't worked.

I develop a component library that use 18next. the consumer of the npm, also use 18next. we created instance for each and pass it down using the provider component.

when i removed .use(initReactI18Next) it corrupted the translation in the component and didn't help in the consumer with the initial issue. any other solution that might work ?

I18n.js file - in the components project:

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

const resources = {
  en: {
    translation: require('./translations/messages_en.json'),
  },
  bg: {
    translation: require('./translations/messages_bg.json'),
  },
}

const widgetInstance = i18n.createInstance();
widgetInstance
  // pass the i18n instance to react-i18next so the t function will be in the context.
  .use(initReactI18next)
  // init i18next
  // for all options read: https://www.i18next.com/overview/configuration-options
  .init({
    resources,
    react: {
      useSuspense: false,
    },
    fallbackLng: 'en',
    debug: true,

    interpolation: {
      escapeValue: false, // not needed for react as it escapes by default
    },
  })
  .then(result => result)
  .catch(error => console.error(error));

export default widgetInstance;

the component using it:

import i18next from '../../../locale/i18next';

....

<I18nextProvider i18n={i18next}>
<div className="myComponent"/>
</ I18nextProvider i18n={i18next}>

i18n.js in the consumer of the components npm library:

import i18next from ‘i18next’;
import {initReactI18next} from “react-i18next”;
const newInstance1 = i18next.createInstance();
 export default function i18n(locale: string) {
   void  newInstance1.use(initReactI18next)
    .use({
     type: ‘backend’,
      read: (
        language: string,
        namespace: string,
        callback: (
          err: any | null,
          translations?: Record<string, string>,
        ) => void,
      ) => {
        return import(`./locales/messages_${locale}.json`)
          .then(translation => {
            callback(null, translation);
          })
          .catch(error => {
            callback(error);
          });
     },
    })
    .init({
      lng: locale,
        fallbackLng: “en”,
        keySeparator: false,
    });
     return newInstance1;
  }

the component using it in the provider in the consumer:

import i18n from ‘../../i18n’;
<I18nextProvider i18n={i18n(locale)}>
    <LibrayComponent {...props} />
    </I18nextProvider>
jamuhl commented 3 years ago

in your component library using the newly created instance don't use .use(initReactI18next) only work with wrapping into the i18nextProvider

ItayTur commented 3 years ago

@jamuhl thx for responding, i dropped the .use(initReactI18next) from the component library. and we have providers on the top component in the library and in the consumer. (we tried also, to have provider only on the consumer) still, not working. any other solution ?

note* - the library component pass the i18n object exported from the i18n.ts, to the provider like this :

import i18next from '../../../locale/i18next';

<I18nextProvider i18n={i18next}> 

while the consumer use it like this:

import i18n from ‘../../i18n’;

<I18nextProvider i18n={i18n(locale)}>
    <Widget {...props} />
    </I18nextProvider>
jamuhl commented 3 years ago

not sure what's going wrong in your setup...can only tell we're doing the same without any issue...even using 2 different backends -> loading two different locize projects...

ItayTur commented 3 years ago

ummmm... thx anyway, ill update when i find whats wrong

pranjalg8 commented 3 years ago

@jamuhl I am using vanillaJS in my package. I want to create 2 instances of i18n and use them with different options. I have a helper file to pull out translations from. Can you suggest me a way where i want to use instances of 18n and use 't' method to call for translations?

import XHR from 'i18next-xhr-backend';
import i18next from 'i18next';
import ICU from 'i18next-icu';
import LngDetector from 'i18next-browser-languagedetector';
//@ts-ignore
import languageMap from '@amzn/katal-localization/dist/webpack/localization-loader!';

export const i18n = i18next
    .use(ICU)
    .use(XHR)
    .use(LngDetector)
    .init(
        {
            debug: false,
            fallbackLng: 'en-US',
            load: 'currentOnly',
            backend: {
                loadPath: (languages: string[]) => {
                    return SERVER_MODE.mode === 'mons'
                        ? `${CLOUD_PATH}${languageMap[languages[0]]}`
                        : `${languageMap[languages[0]]}`;
                }
            },
            detection: {
                order: ['htmlTag', 'navigator']
            }
        },
        (err, t) => {

        }
    );

export const updateElementWithTranslation = (
    elementSelector: string,
    elementType: 'property' | 'attribute',
    elementProperty: string,
    stringId: string,
    defaultString?: string,
    formatArguments?: any) => {

    let translation = getTranslation(stringId, defaultString, formatArguments);
    const element = document.querySelector(elementSelector);
    if (!element) {
        throw new Error(`No element found at path '${elementSelector}'`);
    } else {
        switch (elementType) {
            case 'attribute':
                element.setAttribute(elementProperty, translation);
                break;
            case 'property':
                (element as any)[elementProperty] = translation;
                break;
            default :
                throw new Error(
                    `Unknown property type '${elementType}' (expected 'attribute' or 'property')`
                );
        }
    }
};

const stringDebug = () => {
    let url = window.location.href;
    return url.includes('stringDebug=true') ||
        url.includes('stringDebug=1');
};

export const getTranslation = (
    stringId: string,
    defaultString?: string,
    formatArguments?: any
) => {
    if (!formatArguments) {
        formatArguments = {};
    }
    if (!defaultString) {
        defaultString = "";
    }
    const marketplaceIds = ['SellerCentral', 'default'];
    const strings = [];
    marketplaceIds.forEach(function (item) {
        strings.push(stringId + "." + item);
    });
    strings.push(stringId);
    strings.push(defaultString);
    const translation = i18next.t(strings, formatArguments);
    return stringDebug() ? `${translation}[${stringId}]` : translation;
};
jamuhl commented 3 years ago

@pranjalg8 sorry, but I neither understand your issue - nor what you like to do

pranjalg8 commented 3 years ago

If you see i am calling i18next.t inside getTranslation . Now my requirement is such that i want to use different options, particularly for detection

detection: {
                order: ['queryString','htmlTag', 'navigator']
                lookupQueryString: 'language'
            }

instead of what's already in my file above for a particular page in my app.

So essentially i want to have 2 instances of i18next both with different configurations but in the same app such that i can selectively call particular instance on different pages.

@jamuhl Did you get my query now? I want to have multiple i18next instances, but not in a react app. Mine is in VanillaJS.

Mertakus commented 2 years ago

I've read this thread now multiple times but I still cannot figure out how to create a react app with multiple instances running at the same time. I have created a codesandbox with my problem here: codesandbox demo.

What I am trying to do is have 2 instances of reacti18Next running on the same application. I want parts of my app translated with for example instance1 and other parts with instance2. Whatever I try, I cannot seem to get this to work and I am now even wondering if this is even possible with reactI18Next? Could someone take a look at my codesandbox example and tell me if it is even possible what I am trying to do?

jamuhl commented 2 years ago

@Mertakus using instances is meant for different areas inside an app - not mainly on the same view. By using https://react.i18next.com/latest/i18nextprovider

theoretically, you can use the Provider inside views too - just a lot of work. Or pass in the i18n instance to useTranslation https://github.com/i18next/react-i18next/blob/master/src/useTranslation.js#L7

or create a custom hook using useTranslation and setting that instance eg. useInstance1Translation

ugnelis commented 2 years ago

It worked! I left an example for any future readers:

Edit j27n3k65yw

I appreciate your assistance with my issue and also the great work you have put into i18next!

Hi, the link is not working anymore

gitnupur commented 1 year ago

It worked! I left an example for any future readers:

Edit j27n3k65yw

I appreciate your assistance with my issue and also the great work you have put into i18next!

I can you please share the sandbox link again. I am facing similar issue.

angusryer commented 1 year ago

In my scenario, I need to be able to point the backend from an unauthenticated translation resource to an authenticated one. I'm not sure if this is the best approach, but I created two instances of i18n, each with different backend configs. I initialize the public one immediately on app-load, then, once the user has logged in, I initialize the authenticated one. This works fine in the app, but I cannot get my tests to work.

First: If I want to create two instances of i18n, can they be nested within each other in the same component tree? Second: How can I ensure that my test environment can actually access the unmocked t function so I can verify that the translations (and snapshots) are yielding the correct values?

jamuhl commented 1 year ago

@angusryer simpler would be to create a custom backend that handles both or using https://github.com/i18next/i18next-chained-backend on the same instance

having multiple instances is doable by using: https://react.i18next.com/latest/i18nextprovider (allows also nesting if the right context provider wraps the inner components)

mixing instances in same component could be some extra work as you can't use the provider in that case but have to import the needed instances yourself and use the t function from them

regarding tests...if those are unit tests even if you use the unmocked t...you still somehow inject an i18n instance to the component (using provider or whatever)...I would move those assertions for right i18n usage to e2e tests.

adrai commented 1 year ago

fyi: https://github.com/i18next/react-i18next/tree/master/example/react-component-lib

angusryer commented 1 year ago

@angusryer simpler would be to create a custom backend that handles both or using https://github.com/i18next/i18next-chained-backend on the same instance

having multiple instances is doable by using: https://react.i18next.com/latest/i18nextprovider (allows also nesting if the right context provider wraps the inner components)

mixing instances in same component could be some extra work as you can't use the provider in that case but have to import the needed instances yourself and use the t function from them

regarding tests...if those are unit tests even if you use the unmocked t...you still somehow inject an i18n instance to the component (using provider or whatever)...I would move those assertions for right i18n usage to e2e tests.

Thanks for helping me explore this. I managed to remove the need for two instances by just fetching another translation resource after login and using addResourceBundle to merge it with the existing one from the first instance. This ended up being quite easy 🤗

For tests, after removing one i18n instance, I created a __mock__ folder with a near-copy of my actual i18n config module (but made to be synchronous). Then I had to add jest.mock('../my/custom/i18n') to all my test files

andreev-artem commented 1 year ago

Is that possible to enable typings somehow if multiple instances are used?

adrai commented 1 year ago

Is that possible to enable typings somehow if multiple instances are used?

I don't think so... but maybe @pedrodurek has an idea for the future?

sw-tracker commented 1 year ago

How do you guys mock <I18nextProvider i18n={i18n}>? I keep getting the error:

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.

Even though the component works perfectly during normal operation. I have mocked this so far:

jest.mock('react-i18next', () => ({
  // this mock makes sure any components using the translate hook can use it without a warning being shown
  useTranslation: () => {
    return {
      t: (str: string) => str + MOCK_TRANSLATION_POSTFIX,
      i18n: {
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        changeLanguage: () => new Promise(() => {}),
      },
    };
  },
}));