使用 Module Federation 在 React 微前端中实现国际化 (i18n)

示例参考

在此查看示例项目列表:React i18n

概述

在微前端领域,确保每个应用尽可能保持自治是至关重要的。然而,也会出现不可避免的应用程序间的通信场景。一个常见的需求是在微前端之间同步语言偏好,例如当用户在一个应用中更改语言设置时,该变更会级联至所有集成的微前端。本文档概述了在由 Module Federation 促进的微前端架构中使用 react-i18next 库实现国际化的策略。

场景

考虑有两个应用:应用 A(容器应用)和应用 B(远程应用)。尽管应用 B 作为一个独立的应用独立运行,但它也被设计为在运行时嵌入应用 A 中。我们的目标是在应用 A 和应用 B 之间无缝同步语言设置,确保两个应用都能管理和展示它们的本地化内容。

架构和实现

扩展捆绑器配置

为了适应微前端集成,我们使用 Module Federation 插件扩展现有的 Webpack/Rspack/Rsbuild 和 Create React App (CRA) 配置。此扩展允许应用 B 暴露各种元素(例如组件、主题、钩子)供应用 A 或任何其他集成的应用使用,而不需要代码库驱逐。

设置国际化

react-i18next 实现构成了我们翻译功能的基础。最初,i18next 的一个实例被配置并直接导入到主应用程序文件(App.js)。该实例利用上下文存储来管理状态、资源(翻译)和插件。

解决翻译重写挑战

直接实现方法,每个应用初始化其 i18next 实例可能导致资源冲突,特别是在重写翻译术语的情况下。为了避免这种情况,我们为应用 A 和应用 B 建立独立的 i18next 实例,确保每个应用独立维护其翻译术语。

实施步骤

配置 i18next 实例

对于应用 A 和应用 B,如下配置独立的 i18next 实例:

// 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

集成 i18next 提供商

将应用程序组件包装在“I18nextProvider”中,传递相应的“i18next”实例以确保正确应用翻译上下文。

// 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

语言切换逻辑

对于App B,编写一个自定义的hook,方便语言切换,例如:

import appBInstance from '../i18n';

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

export default useSwitchLanguage;

通过模块联邦公开此挂钩,以允许 App A 或其他集成应用程序使用它。

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

在 App A 中,实现一个钩子来协调所有集成微前端的语言切换:

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 a user interface component, such as a button, to trigger language changes across all applications:

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;

处理集成环境

要有条件地显示语言切换器组件(例如,在嵌入应用程序 A 时隐藏应用程序 B 中的切换器),请利用“useIsRemote”等自定义挂钩。

useIsRemote 钩子旨在确定当前应用程序(例如应用程序 B)是否以独立模式运行或嵌入到另一个应用程序(例如应用程序 A)中。这种区别使我们能够根据应用程序的上下文有条件地渲染组件。

下面我们提供了“useIsRemote”挂钩的简化实现示例,该挂钩检查特定条件以确定应用程序的环境。在实际应用程序中,此条件可能基于 URL 参数、DOM 存在检查或区分嵌入和独立运行的任何其他触发器:

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;

使用 useIsRemote 挂钩

实现 useIsRemote 挂钩后,你现在可以在组件中使用它,根据应用程序是独立运行还是嵌入运行来有条件地渲染元素。以下是如何使用它在 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;

在此示例中,仅当应用程序 B 未嵌入应用程序 A 中时,基于由“useIsRemote”挂钩确定的“isRemote”状态,“LanguageSwitcher”才会呈现。这种方法确保语言切换器等组件仅在适当的上下文中显示,从而增强用户体验并保持微前端应用程序的独立性。