使用 Module Federation 在 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”才会呈现。这种方法确保语言切换器等组件仅在适当的上下文中显示,从而增强用户体验并保持微前端应用程序的独立性。