# 使用 Module Federation 在 React 微前端中实现国际化 (i18n) :::tip 示例参考 在此查看示例项目列表:[React i18n](https://github.com/module-federation/module-federation-examples/tree/master/i18next-nextjs-react) ::: ## 概述 在微前端领域,确保每个应用尽可能保持自治是至关重要的。然而,也会出现不可避免的应用程序间的通信场景。一个常见的需求是在微前端之间同步语言偏好,例如当用户在一个应用中更改语言设置时,该变更会级联至所有集成的微前端。本文档概述了在由 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` 实例: ```javascript // 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”实例以确保正确应用翻译上下文。 ```javascript // 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,方便语言切换,例如: ```javascript import appBInstance from '../i18n'; const useSwitchLanguage = () => { return (languageId) => appBInstance.changeLanguage(languageId); }; export default useSwitchLanguage; ``` 通过模块联邦公开此挂钩,以允许 App A 或其他集成应用程序使用它。 ```javascript // Module Federation exposes configuration exposes: { './hooks/useSwitchAppBLanguage': './src/hooks/useSwitchLanguage', }, ``` 在 App A 中,实现一个钩子来协调所有集成微前端的语言切换: ```javascript 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: ```javascript 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 存在检查或区分嵌入和独立运行的任何其他触发器: ```javascript 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 中有条件地显示语言切换器组件的示例: ```javascript 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”才会呈现。这种方法确保语言切换器等组件仅在适当的上下文中显示,从而增强用户体验并保持微前端应用程序的独立性。