Implementing Internationalization (i18n) com Module Federation em React Micro-Frontends

Demo Reference

Check out o exemplo projeto list here: React i18n

Visão geral

No realm de micro-frontends, ensuring each aplicação remains as autonomous as possible é crucial. However, scenarios arise onde inter-aplicação communication é inevitable. Uma comum requirement é synchronizing language preferences across micro-frontends, such as quando uma user changes o language setting em one aplicação, e it cascades across all integrated micro-frontends. Este documentação outlines uma estratégia for implementing internationalization usando o react-i18next library within uma micro-frontend architecture facilitated by Module Federation.

Scenario

Consider two aplicações: App A (o container aplicação) e App B (a aplicação remote). While App B funções independently as uma standalone aplicação, it é também designed para ser embedded within App A em runtime. Our objective é para seamlessly synchronize language settings entre App A e App B, ensuring both aplicações pode manage e display their localized content.

Architecture e Implementation

Extending Bundler Configuração

To accommodate micro-frontend integração, nós extend o existente Webpack/Rspack/Rsbuild e Create React App (CRA) configurações usando o Module Federation plugin. Este extension allows App B para exponha various elements (e.g., componentes, themes, hooks) for consumption by App A ou any other integrated aplicações sem requiring codebase ejection.

Setting Up Internationalization

O react-i18next implementation forms o backbone de our translation functionality. Initially, um instance de i18next é configured e imported diretamente no principal aplicação arquivo (App.js). Este instance leverages uma context store para manage estado, recursos (translations), e plugins.

Addressing Translation Override Challenges

Uma direct implementation approach onde each aplicação initializes its i18next instance could lead para recurso conflicts, particularly onde translated terms são overridden. To avoid este, nós establish separate i18next instances for App A e App B, ensuring each aplicação maintains its translation terms independently.

Implementation Steps

Configuring i18next Instances

For both App A e App B, configure separate i18next instances as follows:

// App A
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enJSON from './translations/en';
import uaJSON from './translations/ua';

// Translation resources
const resources = {
  en: { translation: enJSON },
  ua: { translation: uaJSON },
};

// Initialize i18next instance for App A
const appAInstance = i18n.createInstance();
appAInstance.use(initReactI18next).init({
  resources,
  lng: 'en', // default language
  fallbackLng: 'en',
  interpolation: { escapeValue: false },
  react: { useSuspense: true },
});

export default appAInstance;

// Repeat similar setup for App B with a separate instance

Integrating i18next Providers

Wrap a aplicação componentes em I18nextProvider, passing o respective i18next instances para garantir translation contexts são correctly applied.

// App A Wrapper
import { I18nextProvider } from 'react-i18next';
import appAInstance from '../i18n';

const AppAI18nWrapper = ({ children }) => (
  <I18nextProvider i18n={appAInstance}>{children}</I18nextProvider>
);

export default AppAI18nWrapper;

// Repeat similar setup for App B

Language Switching Logic

For App B, write uma custom hook para facilitate language switching, por exemplo:

import appBInstance from '../i18n';

const useSwitchLanguage = () => {
  return (languageId) => appBInstance.changeLanguage(languageId);
};

export default useSwitchLanguage;

Exponha este hook via Module Federation para allow its consumption by App A ou other integrated aplicações.

// Module Federation exposes configuration
exposes: {
  './hooks/useSwitchAppBLanguage': './src/hooks/useSwitchLanguage',
},

In App A, implement uma hook esse orchestrates language switching across all integrated micro-frontends:

import useSwitchAppBLanguage from 'remoteAppB/hooks/useSwitchAppBLanguage';
import appAInstance from '../i18n';

const useSwitchLanguage = () => {
  const switchAppBLanguageHook = useSwitchAppBLanguage();
  //Application A
  const switchAppALanguage = (languageCode) => appAInstance.changeLanguage(languageCode);
  //Application B
  const switchAppBLanguage = (languageCode) => switchAppBLanguageHook(languageCode);
  //Both Applications
  const switchAllLanguages = (languageCode) => {
    switchAppALanguage(languageCode);
    switchAppBLanguage(languageCode);
  };

  return { switchAppALanguage, switchAppBLanguage, switchAllLanguages };
};

export default useSwitchLanguage;

Language Switching Interface

Implement uma user interface componente, such as uma button, para trigger language changes across all aplicações:

import { useSwitchLanguage } from 'src/hooks/useSwitchLanguage';

const LanguageSwitcher = () => {
  const { switchAllLanguages } = useSwitchLanguage();
  const handleLanguageSwitch = (lng) => () => switchAllLanguages(lng);

  return <button onClick={handleLanguageSwitch("ua")}>Change language to Ukrainian</button>;
};

export default LanguageSwitcher;

Handling Integrated Environments

To conditionally display language switcher componentes (e.g., hide o switcher em App B quando embedded em App A), leverage uma custom hook like useIsRemote.

O useIsRemote hook é designed para determine se o current aplicação (e.g., App B) é running em uma standalone mode ou é embedded within another aplicação (e.g., App A). Este distinction allows us para conditionally render componentes based no aplicação's context.

Below nós fornecido um exemplo de uma simplified implementation do useIsRemote hook esse checks para uma specific condition para determine a aplicação's environment. No real aplicação este condition could ser baseado em URL parameters, DOM presence checks, ou any other trigger esse differentiates entre being embedded e running standalone:

import { useEffect, useState } from 'react';

/**
 * Determines if the current application is running as a remote (embedded)
 * or as a standalone application.
 *
 * You should adapt the logic based on the specific criteria that apply to your application's
 * architecture, such as checking for specific URL parameters or the presence
 * of a particular DOM element that would only exist when embedded.
 */
const useIsRemote = () => {
  const [isRemote, setIsRemote] = useState(false);

  useEffect(() => {
    // Check for a URL parameter that indicates embedding
    const searchParams = new URLSearchParams(window.location.search);
    setIsRemote(searchParams.has('embedded'));

    // Alternatively, check for a global variable or a specific DOM element
    // setIsRemote(window.parent !== window || document.getElementById('embed-flag') !== null);
  }, []);

  return isRemote;
};

export default useIsRemote;

Usando o useIsRemote Hook

Com o useIsRemote hook implemented, você pode now use it within your componentes para conditionally render elements baseado em whether a aplicação é running standalone ou é embedded. Here's um exemplo de como it might ser usado para conditionally display uma language switcher componente em App B:

import React from 'react';
import useIsRemote from './hooks/useIsRemote';
import LanguageSwitcher from './components/LanguageSwitcher';

const App = () => {
  const isRemote = useIsRemote();

  return (
    <div>
      {/* Only display the LanguageSwitcher if not running as a remote */}
      {!isRemote && <LanguageSwitcher />}
    </div>
  );
};

export default App;

In este exemplo, LanguageSwitcher vai apenas render quando App B é não embedded within App A, based no isRemote estado determined pelo useIsRemote hook. Este approach ensures esse componentes like language switchers são apenas shown em appropriate contexts, enhancing o user experience e maintaining o independence de micro-frontend aplicações.