Skip to content

Commit 9245fcc

Browse files
committed
feat: i18n plugin support merge custom plugin config && op wrap root
1 parent 8a30828 commit 9245fcc

File tree

3 files changed

+77
-24
lines changed

3 files changed

+77
-24
lines changed

packages/runtime/plugin-i18n/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@modern-js/plugin": "workspace:*",
8989
"@modern-js/server-core": "workspace:*",
9090
"@modern-js/server-runtime": "workspace:*",
91+
"@modern-js/runtime-utils": "workspace:*",
9192
"@modern-js/types": "workspace:*",
9293
"@modern-js/utils": "workspace:*",
9394
"@swc/helpers": "^0.5.17",

packages/runtime/plugin-i18n/src/runtime/index.tsx

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {
44
isBrowser,
55
useRuntimeContext,
66
} from '@modern-js/runtime';
7+
import { merge } from '@modern-js/runtime-utils/merge';
78
import type React from 'react';
8-
import { useEffect, useState } from 'react';
9+
import { useEffect, useMemo, useRef, useState } from 'react';
910
import type {
1011
BaseBackendOptions,
1112
BaseLocaleDetectionOptions,
@@ -43,13 +44,17 @@ export interface I18nPluginOptions {
4344
[key: string]: any;
4445
}
4546

46-
const getPathname = (context: TRuntimeContext) => {
47+
const getPathname = (context: TRuntimeContext): string => {
4748
if (isBrowser()) {
4849
return window.location.pathname;
4950
}
5051
return context.ssrContext?.request?.pathname || '/';
5152
};
5253

54+
interface RuntimeContextWithI18n extends TRuntimeContext {
55+
i18nInstance?: I18nInstance;
56+
}
57+
5358
export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
5459
name: '@modern-js/plugin-i18n',
5560
setup: api => {
@@ -73,6 +78,9 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
7378

7479
api.onBeforeRender(async context => {
7580
let i18nInstance = await getI18nInstance(userI18nInstance);
81+
const { i18n: otherConfig } = api.getRuntimeConfig();
82+
const { initOptions: otherInitOptions } = otherConfig || {};
83+
const initOptions = merge(otherInitOptions || {}, userInitOptions || {});
7684
const initReactI18next = await getInitReactI18next();
7785
I18nextProvider = await getI18nextProvider();
7886
if (initReactI18next) {
@@ -90,9 +98,9 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
9098
i18nextDetector,
9199
detection,
92100
localePathRedirect,
93-
userInitOptions,
101+
initOptions,
94102
);
95-
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
103+
const mergedBackend = mergeBackendOptions(backend, initOptions);
96104

97105
// Register Backend before init() if needed
98106
if (backendEnabled) {
@@ -106,7 +114,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
106114
localePathRedirect,
107115
i18nextDetector,
108116
detection,
109-
userInitOptions,
117+
userInitOptions: initOptions,
110118
pathname,
111119
ssrContext: context.ssrContext,
112120
});
@@ -119,7 +127,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
119127
languages,
120128
mergedDetection,
121129
mergedBackend,
122-
userInitOptions,
130+
initOptions,
123131
);
124132

125133
if (!isBrowser() && i18nInstance.cloneInstance) {
@@ -134,7 +142,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
134142
i18nextDetector,
135143
detection,
136144
localePathRedirect,
137-
userInitOptions,
145+
initOptions,
138146
);
139147
}
140148

@@ -150,21 +158,37 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
150158

151159
api.wrapRoot(App => {
152160
return props => {
153-
const runtimeContext = useRuntimeContext();
154-
const i18nInstance = (runtimeContext as any).i18nInstance;
155-
const initialLang =
156-
i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en');
161+
const runtimeContext = useRuntimeContext() as RuntimeContextWithI18n;
162+
const i18nInstance = runtimeContext.i18nInstance;
163+
const initialLang = useMemo(
164+
() =>
165+
i18nInstance?.language ||
166+
(localeDetection?.fallbackLanguage ?? 'en'),
167+
[i18nInstance?.language, localeDetection?.fallbackLanguage],
168+
);
157169
const [lang, setLang] = useState(initialLang);
158170

159-
if (i18nInstance?.language && i18nInstance.translator) {
160-
i18nInstance.translator.language = i18nInstance.language;
161-
}
171+
// Sync translator language with i18n instance language
172+
useEffect(() => {
173+
if (i18nInstance?.language) {
174+
const translator = (i18nInstance as any).translator;
175+
if (translator) {
176+
translator.language = i18nInstance.language;
177+
}
178+
}
179+
}, [i18nInstance?.language]);
180+
181+
// Handle language detection and updates
182+
const runtimeContextRef = useRef(runtimeContext);
183+
runtimeContextRef.current = runtimeContext;
162184

163185
useEffect(() => {
186+
if (!i18nInstance) {
187+
return;
188+
}
189+
164190
if (localePathRedirect) {
165-
const currentPathname = getPathname(
166-
runtimeContext as TRuntimeContext,
167-
);
191+
const currentPathname = getPathname(runtimeContextRef.current);
168192
const pathDetection = detectLanguageFromPath(
169193
currentPathname,
170194
languages,
@@ -174,8 +198,8 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
174198
const currentLang = pathDetection.language;
175199
if (currentLang !== lang) {
176200
setLang(currentLang);
177-
i18nInstance?.setLang?.(currentLang);
178-
i18nInstance?.changeLanguage?.(currentLang);
201+
i18nInstance.setLang?.(currentLang);
202+
i18nInstance.changeLanguage?.(currentLang);
179203
if (isBrowser()) {
180204
const detectionOptions = i18nInstance.options?.detection;
181205
cacheUserLanguage(
@@ -192,24 +216,49 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
192216
setLang(instanceLang);
193217
}
194218
}
195-
}, []);
219+
}, [i18nInstance, localePathRedirect, languages, lang]);
196220

197-
const contextValue = {
198-
language: lang,
221+
const contextValue = useMemo(() => {
222+
if (!i18nInstance) {
223+
// Return a minimal context value when i18nInstance is not available
224+
return {
225+
language: lang,
226+
i18nInstance: {} as I18nInstance,
227+
entryName,
228+
languages,
229+
localePathRedirect,
230+
ignoreRedirectRoutes,
231+
updateLanguage: setLang,
232+
};
233+
}
234+
return {
235+
language: lang,
236+
i18nInstance,
237+
entryName,
238+
languages,
239+
localePathRedirect,
240+
ignoreRedirectRoutes,
241+
updateLanguage: setLang,
242+
};
243+
}, [
244+
lang,
199245
i18nInstance,
200246
entryName,
201247
languages,
202248
localePathRedirect,
203249
ignoreRedirectRoutes,
204-
updateLanguage: setLang,
205-
};
250+
]);
206251

207252
const appContent = (
208253
<ModernI18nProvider value={contextValue}>
209254
<App {...props} />
210255
</ModernI18nProvider>
211256
);
212257

258+
if (!i18nInstance) {
259+
return appContent;
260+
}
261+
213262
return I18nextProvider ? (
214263
<I18nextProvider i18n={i18nInstance}>{appContent}</I18nextProvider>
215264
) : (

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)