Skip to content

Commit 20a7af4

Browse files
caohuilinyimingjfe
andauthored
feat: i18n plugin mf cases (#7898)
Co-authored-by: Ming <[email protected]>
1 parent a40932d commit 20a7af4

File tree

59 files changed

+1764
-39
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1764
-39
lines changed

biome.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"packages/generator/sandpack-react/src/templates/**",
1717
"tests/integration/swc/fixtures/minify-css/src/bootstrap.css",
1818
"tests/integration/swc/fixtures/config-function/src/bootstrap.css",
19-
"packages/runtime/plugin-runtime/static/**"
19+
"packages/runtime/plugin-runtime/static/**",
20+
"tests/integration/i18n/mf-consumer/@mf-types/**"
2021
]
2122
},
2223
"css": {
@@ -113,7 +114,8 @@
113114
"tests/integration/module/plugins/vue/**/*",
114115
"packages/module/plugin-module-node-polyfill/src/globals.js",
115116
"packages/runtime/plugin-runtime/static/**",
116-
"packages/cli/flight-server-transform-plugin/tests/fixture/**/*"
117+
"packages/cli/flight-server-transform-plugin/tests/fixture/**/*",
118+
"**/@mf-types/**"
117119
]
118120
}
119121
}

packages/cli/builder/src/shared/parseCommonConfig.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export async function parseCommonConfig(
7070
html: { outputStructure, appIcon, ...htmlConfig } = {},
7171
source: { alias, globalVars, transformImport, ...sourceConfig } = {},
7272
dev = {},
73+
server = {},
7374
security: { checkSyntax, sri, ...securityConfig } = {},
7475
tools: {
7576
devServer,
@@ -188,7 +189,7 @@ export async function parseCommonConfig(
188189
const { rsbuildDev, rsbuildServer } = transformToRsbuildServerOptions(
189190
dev || {},
190191
devServer || {},
191-
builderConfig.server,
192+
server || {},
192193
);
193194

194195
rsbuildConfig.server = removeUndefinedKey(rsbuildServer);

packages/runtime/plugin-i18n/src/cli/index.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export interface I18nPluginOptions {
1313
localeDetection?: LocaleDetectionOptions;
1414
backend?: BackendOptions;
1515
transformRuntimeConfig?: TransformRuntimeConfigFn;
16+
customPlugin?: {
17+
runtime?: {
18+
name?: string;
19+
path?: string;
20+
};
21+
server?: {
22+
name?: string;
23+
};
24+
};
1625
[key: string]: any;
1726
}
1827

@@ -21,8 +30,13 @@ export const i18nPlugin = (
2130
): CliPlugin<AppTools> => ({
2231
name: '@modern-js/plugin-i18n',
2332
setup: api => {
24-
const { localeDetection, backend, transformRuntimeConfig, ...restOptions } =
25-
options;
33+
const {
34+
localeDetection,
35+
backend,
36+
transformRuntimeConfig,
37+
customPlugin,
38+
...restOptions
39+
} = options;
2640
api._internalRuntimePlugins(({ entrypoint, plugins }) => {
2741
const localeDetectionOptions = localeDetection
2842
? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
@@ -50,8 +64,8 @@ export const i18nPlugin = (
5064
};
5165

5266
plugins.push({
53-
name: 'i18n',
54-
path: `@${metaName}/plugin-i18n/runtime`,
67+
name: customPlugin?.runtime?.name || 'i18n',
68+
path: customPlugin?.runtime?.path || `@${metaName}/plugin-i18n/runtime`,
5569
config,
5670
});
5771
return {
@@ -88,7 +102,7 @@ export const i18nPlugin = (
88102
});
89103

90104
plugins.push({
91-
name: `@${metaName}/plugin-i18n/server`,
105+
name: customPlugin?.server?.name || `@${metaName}/plugin-i18n/server`,
92106
options: {
93107
localeDetection,
94108
staticRoutePrefixes,

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

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ModernI18nContextValue {
1111
entryName?: string;
1212
languages?: string[];
1313
localePathRedirect?: boolean;
14+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
1415
// Callback to update language in context
1516
updateLanguage?: (newLang: string) => void;
1617
}
@@ -41,6 +42,44 @@ export interface UseModernI18nReturn {
4142
isLanguageSupported: (lang: string) => boolean;
4243
}
4344

45+
/**
46+
* Check if the given pathname should ignore automatic locale redirect
47+
*/
48+
const shouldIgnoreRedirect = (
49+
pathname: string,
50+
languages: string[],
51+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
52+
): boolean => {
53+
if (!ignoreRedirectRoutes) {
54+
return false;
55+
}
56+
57+
// Remove language prefix if present (e.g., /en/api -> /api)
58+
const segments = pathname.split('/').filter(Boolean);
59+
let pathWithoutLang = pathname;
60+
if (segments.length > 0 && languages.includes(segments[0])) {
61+
// Remove language prefix
62+
pathWithoutLang = `/${segments.slice(1).join('/')}`;
63+
}
64+
65+
// Normalize path (ensure it starts with /)
66+
const normalizedPath = pathWithoutLang.startsWith('/')
67+
? pathWithoutLang
68+
: `/${pathWithoutLang}`;
69+
70+
if (typeof ignoreRedirectRoutes === 'function') {
71+
return ignoreRedirectRoutes(normalizedPath);
72+
}
73+
74+
// Check if pathname matches any of the ignore patterns
75+
return ignoreRedirectRoutes.some(pattern => {
76+
// Support both exact match and prefix match
77+
return (
78+
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
79+
);
80+
});
81+
};
82+
4483
// Safe hook wrapper to handle cases where router context is not available
4584
const useRouterHooks = () => {
4685
try {
@@ -91,6 +130,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
91130
i18nInstance,
92131
languages,
93132
localePathRedirect,
133+
ignoreRedirectRoutes,
94134
updateLanguage,
95135
} = context;
96136

@@ -143,33 +183,55 @@ export const useModernI18n = (): UseModernI18nReturn => {
143183
const entryPath = getEntryPath();
144184
const relativePath = currentPath.replace(entryPath, '');
145185

146-
// Build new path with updated language
147-
const newPath = buildLocalizedUrl(
148-
relativePath,
149-
newLang,
150-
languages || [],
151-
);
152-
const newUrl = entryPath + newPath + location.search + location.hash;
186+
// Check if this route should ignore automatic redirect
187+
if (
188+
!shouldIgnoreRedirect(
189+
relativePath,
190+
languages || [],
191+
ignoreRedirectRoutes,
192+
)
193+
) {
194+
// Build new path with updated language
195+
const newPath = buildLocalizedUrl(
196+
relativePath,
197+
newLang,
198+
languages || [],
199+
);
200+
const newUrl =
201+
entryPath + newPath + location.search + location.hash;
153202

154-
// Navigate to new URL
155-
navigate(newUrl, { replace: true });
203+
// Navigate to new URL
204+
await navigate(newUrl, { replace: true });
205+
}
156206
} else if (localePathRedirect && isBrowser() && !hasRouter) {
157207
// Fallback: use window.history API when router is not available
158208
const currentPath = window.location.pathname;
159209
const entryPath = getEntryPath();
160210
const relativePath = currentPath.replace(entryPath, '');
161211

162-
// Build new path with updated language
163-
const newPath = buildLocalizedUrl(
164-
relativePath,
165-
newLang,
166-
languages || [],
167-
);
168-
const newUrl =
169-
entryPath + newPath + window.location.search + window.location.hash;
170-
171-
// Use history API to navigate without page reload
172-
window.history.pushState(null, '', newUrl);
212+
// Check if this route should ignore automatic redirect
213+
if (
214+
!shouldIgnoreRedirect(
215+
relativePath,
216+
languages || [],
217+
ignoreRedirectRoutes,
218+
)
219+
) {
220+
// Build new path with updated language
221+
const newPath = buildLocalizedUrl(
222+
relativePath,
223+
newLang,
224+
languages || [],
225+
);
226+
const newUrl =
227+
entryPath +
228+
newPath +
229+
window.location.search +
230+
window.location.hash;
231+
232+
// Use history API to navigate without page reload
233+
window.history.pushState(null, '', newUrl);
234+
}
173235
}
174236

175237
// Update language state after URL update
@@ -185,6 +247,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
185247
i18nInstance,
186248
updateLanguage,
187249
localePathRedirect,
250+
ignoreRedirectRoutes,
188251
languages,
189252
hasRouter,
190253
navigate,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface I18nPluginOptions {
3939
i18nInstance?: I18nInstance;
4040
changeLanguage?: (lang: string) => void;
4141
initOptions?: I18nInitOptions;
42+
[key: string]: any;
4243
}
4344

4445
const getPathname = (context: TRuntimeContext) => {
@@ -64,6 +65,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
6465
languages = [],
6566
fallbackLanguage = 'en',
6667
detection,
68+
ignoreRedirectRoutes,
6769
} = localeDetection || {};
6870
const { enabled: backendEnabled = false } = backend || {};
6971
let I18nextProvider: React.FunctionComponent<any> | null;
@@ -184,6 +186,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
184186
entryName,
185187
languages,
186188
localePathRedirect,
189+
ignoreRedirectRoutes,
187190
updateLanguage: setLang,
188191
};
189192

packages/runtime/plugin-i18n/src/server/index.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ const convertToHonoLanguageDetectorOptions = (
7878
};
7979
};
8080

81+
/**
82+
* Check if the given pathname should ignore automatic locale redirect
83+
*/
84+
const shouldIgnoreRedirect = (
85+
pathname: string,
86+
urlPath: string,
87+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
88+
): boolean => {
89+
if (!ignoreRedirectRoutes) {
90+
return false;
91+
}
92+
93+
// Remove urlPath prefix to get remaining path for matching
94+
const basePath = urlPath.replace('/*', '');
95+
const remainingPath = pathname.startsWith(basePath)
96+
? pathname.slice(basePath.length)
97+
: pathname;
98+
99+
// Normalize path (ensure it starts with /)
100+
const normalizedPath = remainingPath.startsWith('/')
101+
? remainingPath
102+
: `/${remainingPath}`;
103+
104+
if (typeof ignoreRedirectRoutes === 'function') {
105+
return ignoreRedirectRoutes(normalizedPath);
106+
}
107+
108+
// Check if pathname matches any of the ignore patterns
109+
return ignoreRedirectRoutes.some(pattern => {
110+
// Support both exact match and prefix match
111+
return (
112+
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
113+
);
114+
});
115+
};
116+
81117
/**
82118
* Check if the given pathname is a static resource request
83119
* This includes:
@@ -206,6 +242,7 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
206242
languages = [],
207243
fallbackLanguage = 'en',
208244
detection,
245+
ignoreRedirectRoutes,
209246
} = getLocaleDetectionOptions(entryName, options.localeDetection);
210247
const staticRoutePrefixes = options.staticRoutePrefixes;
211248
const originUrlPath = route.urlPath;
@@ -262,6 +299,13 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
262299
return await next();
263300
}
264301

302+
// Check if this route should ignore automatic redirect
303+
if (
304+
shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)
305+
) {
306+
return await next();
307+
}
308+
265309
const language = getLanguageFromPath(c.req, urlPath, languages);
266310
if (!language) {
267311
// Get detected language from languageDetector middleware

packages/runtime/plugin-i18n/src/shared/type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface BaseLocaleDetectionOptions {
99
languages?: string[];
1010
fallbackLanguage?: string;
1111
detection?: LanguageDetectorOptions;
12+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
1213
}
1314

1415
export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {

packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,11 @@ export const routerPlugin = (
147147
return match || '/';
148148
};
149149

150+
// Cache router instance in closure to avoid recreating on parent re-render
151+
let cachedRouter: any = null;
152+
150153
const RouterWrapper = (props: any) => {
151-
const { router, routes } = useRouterCreation(
154+
const routerResult = useRouterCreation(
152155
{
153156
...props,
154157
rscPayload: props?.rscPayload,
@@ -162,6 +165,18 @@ export const routerPlugin = (
162165
},
163166
);
164167

168+
// Only cache router instance, routes are always from routerResult
169+
// rscPayload is stable after first render, so we only create router once
170+
const router = useMemo(() => {
171+
if (cachedRouter) {
172+
return cachedRouter;
173+
}
174+
175+
cachedRouter = routerResult.router;
176+
return cachedRouter;
177+
}, []);
178+
const { routes } = routerResult;
179+
165180
useEffect(() => {
166181
routesContainer.current = routes;
167182
}, [routes]);

packages/server/server/src/dev.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ export const devPlugin = (
110110

111111
middlewares.push({
112112
name: 'mock-dev',
113-
114113
handler: mockMiddleware,
115114
});
116115

packages/toolkit/utils/src/universal/constants.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ export const ROUTE_MANIFEST = `_MODERNJS_ROUTE_MANIFEST`;
88
*/
99
export const ROUTE_MODULES = `_routeModules`;
1010

11-
/**
12-
* hmr socket connect path
13-
*/
14-
export const HMR_SOCK_PATH = '/webpack-hmr';
15-
1611
/**
1712
* html placeholder
1813
*/

0 commit comments

Comments
 (0)