@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +47 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +30 -73
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/contexts.js +49 -0
  7. package/dist/cjs/runtime/core.js +27 -14
  8. package/dist/cjs/runtime/hooks.js +7 -8
  9. package/dist/cjs/runtime/localizedPaths.js +11 -9
  10. package/dist/cjs/runtime/navigation.js +48 -0
  11. package/dist/cjs/runtime/providerComposition.js +37 -13
  12. package/dist/cjs/runtime/routerAdapter.js +33 -140
  13. package/dist/cjs/runtime/sharedContexts.js +43 -0
  14. package/dist/cjs/runtime/utils.js +21 -19
  15. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  16. package/dist/cjs/server/index.js +11 -7
  17. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  18. package/dist/cjs/server/redirectPolicy.js +33 -24
  19. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  20. package/dist/cjs/shared/urlStrategy.js +48 -0
  21. package/dist/esm/cli/index.mjs +10 -21
  22. package/dist/esm/cli/localisedRoutes.mjs +9 -0
  23. package/dist/esm/runtime/Link.mjs +19 -32
  24. package/dist/esm/runtime/context.mjs +20 -72
  25. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  26. package/dist/esm/runtime/contexts.mjs +5 -0
  27. package/dist/esm/runtime/core.mjs +13 -6
  28. package/dist/esm/runtime/hooks.mjs +7 -8
  29. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  30. package/dist/esm/runtime/navigation.mjs +7 -0
  31. package/dist/esm/runtime/providerComposition.mjs +38 -14
  32. package/dist/esm/runtime/routerAdapter.mjs +28 -141
  33. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  34. package/dist/esm/runtime/utils.mjs +19 -17
  35. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  36. package/dist/esm/server/index.mjs +11 -7
  37. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  38. package/dist/esm/server/redirectPolicy.mjs +33 -24
  39. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  40. package/dist/esm/shared/urlStrategy.mjs +7 -0
  41. package/dist/esm-node/cli/index.mjs +10 -21
  42. package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
  43. package/dist/esm-node/runtime/Link.mjs +19 -32
  44. package/dist/esm-node/runtime/context.mjs +20 -72
  45. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  46. package/dist/esm-node/runtime/contexts.mjs +6 -0
  47. package/dist/esm-node/runtime/core.mjs +13 -6
  48. package/dist/esm-node/runtime/hooks.mjs +7 -8
  49. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  50. package/dist/esm-node/runtime/navigation.mjs +8 -0
  51. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  52. package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
  53. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  54. package/dist/esm-node/runtime/utils.mjs +19 -17
  55. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  56. package/dist/esm-node/server/index.mjs +11 -7
  57. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  58. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  59. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  60. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  61. package/dist/types/cli/localisedRoutes.d.ts +19 -0
  62. package/dist/types/runtime/context.d.ts +8 -11
  63. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  64. package/dist/types/runtime/contexts.d.ts +6 -0
  65. package/dist/types/runtime/core.d.ts +12 -2
  66. package/dist/types/runtime/hooks.d.ts +4 -5
  67. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  68. package/dist/types/runtime/navigation.d.ts +10 -0
  69. package/dist/types/runtime/providerComposition.d.ts +13 -1
  70. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  71. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  72. package/dist/types/runtime/utils.d.ts +4 -12
  73. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  74. package/dist/types/server/index.d.ts +2 -0
  75. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  76. package/dist/types/server/redirectPolicy.d.ts +5 -13
  77. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  78. package/dist/types/shared/type.d.ts +7 -11
  79. package/dist/types/shared/urlStrategy.d.ts +18 -0
  80. package/package.json +42 -19
  81. package/rslib.config.mts +8 -1
  82. package/rstest.config.mts +6 -5
  83. package/src/cli/index.ts +42 -42
  84. package/src/cli/localisedRoutes.ts +53 -0
  85. package/src/runtime/Link.tsx +20 -40
  86. package/src/runtime/context.tsx +33 -157
  87. package/src/runtime/contextHelpers.ts +7 -5
  88. package/src/runtime/contexts.ts +16 -0
  89. package/src/runtime/core.tsx +53 -6
  90. package/src/runtime/hooks.ts +14 -9
  91. package/src/runtime/i18n/instance.ts +1 -2
  92. package/src/runtime/localizedPaths.ts +24 -19
  93. package/src/runtime/navigation.tsx +20 -0
  94. package/src/runtime/providerComposition.tsx +71 -23
  95. package/src/runtime/routerAdapter.tsx +65 -339
  96. package/src/runtime/sharedContexts.ts +8 -0
  97. package/src/runtime/utils.ts +49 -31
  98. package/src/server/defaultUrlStrategy.ts +27 -0
  99. package/src/server/index.ts +39 -9
  100. package/src/server/mappedUrlStrategy.ts +10 -0
  101. package/src/server/redirectPolicy.ts +85 -70
  102. package/src/shared/mappedUrlStrategy.ts +59 -0
  103. package/src/shared/type.ts +7 -11
  104. package/src/shared/urlStrategy.ts +38 -0
  105. package/tests/backendDefaults.test.ts +2 -23
  106. package/tests/defaultUrlStrategy.test.ts +107 -0
  107. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  108. package/tests/i18nUtils.test.ts +31 -65
  109. package/tests/link.test.tsx +13 -511
  110. package/tests/localisedRouteGeneration.test.ts +200 -0
  111. package/tests/localisedUrls.test.ts +16 -208
  112. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  113. package/tests/navigation.test.tsx +403 -0
  114. package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
  115. package/tests/redirectPolicy.test.ts +89 -27
  116. package/tests/routerAdapter.test.tsx +98 -1171
  117. package/tests/urlStrategySerialization.test.ts +122 -0
  118. package/tsconfig.json +4 -2
  119. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  120. package/tests/federatedI18nBoundary.test.tsx +0 -132
  121. package/tests/linkTypes.test.ts +0 -59
  122. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  123. package/tests/peerCohort.test.ts +0 -25
  124. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  125. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  126. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  127. package/tests/type-fixture/tsconfig.json +0 -16
@@ -1,14 +1,8 @@
1
+ import { I18nNavigationContext } from '@modern-js/plugin-i18n/runtime/contexts';
1
2
  import { isBrowser, RuntimeContext } from '@modern-js/runtime';
2
- import {
3
- getRouterRuntimeState,
4
- InternalRuntimeContext,
5
- type TInternalRuntimeContext,
6
- type TRuntimeContext,
7
- } from '@modern-js/runtime/context';
3
+ import { InternalRuntimeContext } from '@modern-js/runtime/context';
8
4
  import type React from 'react';
9
- import { useCallback, useContext, useSyncExternalStore } from 'react';
10
-
11
- type I18nRouterFramework = 'react-router' | 'tanstack' | string;
5
+ import { useContext } from 'react';
12
6
 
13
7
  interface I18nRouterLocation {
14
8
  pathname: string;
@@ -16,14 +10,9 @@ interface I18nRouterLocation {
16
10
  hash: string;
17
11
  }
18
12
 
19
- interface I18nRouterNavigateOptions {
20
- replace?: boolean;
21
- state?: unknown;
22
- }
23
-
24
13
  type I18nRouterNavigate = (
25
14
  href: string,
26
- options?: I18nRouterNavigateOptions,
15
+ options?: { replace?: boolean; state?: unknown },
27
16
  ) => void | Promise<void>;
28
17
 
29
18
  type I18nRouterLink = React.ComponentType<{
@@ -32,345 +21,82 @@ type I18nRouterLink = React.ComponentType<{
32
21
  [key: string]: unknown;
33
22
  }>;
34
23
 
35
- interface I18nRouterAdapter {
36
- framework?: I18nRouterFramework;
24
+ export interface I18nRouterLinkTarget {
25
+ pathname: string;
26
+ href: string;
27
+ search?: Record<string, unknown>;
28
+ hash?: string;
29
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions;
30
+ prefetch?: 'intent' | 'render' | 'viewport' | 'none';
31
+ preload?: unknown;
32
+ }
33
+
34
+ export interface I18nRouterAdapter {
35
+ framework?: string;
37
36
  hasRouter: boolean;
38
37
  location: I18nRouterLocation | null;
39
38
  navigate: I18nRouterNavigate | null;
40
39
  Link: I18nRouterLink | null;
41
40
  params: Record<string, string>;
42
- }
43
-
44
- type RuntimeContextWithRouter = TRuntimeContext & {
45
- router?: {
46
- useInRouterContext?: () => boolean;
47
- useRouter?: (options?: { warn?: boolean }) => unknown;
48
- useLocation?: () => unknown;
49
- useNavigate?: () => I18nRouterNavigate;
50
- useParams?: () => Record<string, string>;
51
- useHref?: () => unknown;
52
- Link?: I18nRouterLink;
53
- };
54
- };
55
-
56
- type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
57
- router?: RuntimeContextWithRouter['router'];
58
- };
59
-
60
- type RouterInstance = {
61
- navigate?: (...args: any[]) => unknown;
62
- state?: {
63
- location?: unknown;
64
- matches?: Array<{ params?: Record<string, string> }>;
65
- };
66
- stores?: {
67
- location?: {
68
- get?: () => unknown;
69
- subscribe?: (listener: () => void) => () => void;
70
- };
71
- matches?: {
72
- get?: () => Array<{ params?: Record<string, string> }>;
73
- };
74
- };
75
- subscribe?:
76
- | ((listener: () => void) => () => void)
77
- | ((eventType: string, listener: () => void) => () => void);
78
- };
79
-
80
- const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
81
- if (typeof value !== 'string' || !value) {
82
- return '';
83
- }
84
- return value.startsWith(prefix) ? value : `${prefix}${value}`;
85
- };
86
-
87
- const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
88
- if (!location || typeof location !== 'object') {
89
- return null;
90
- }
91
-
92
- const locationValue = location as {
93
- pathname?: unknown;
94
- search?: unknown;
95
- searchStr?: unknown;
96
- hash?: unknown;
97
- };
98
-
99
- if (typeof locationValue.pathname !== 'string') {
100
- return null;
101
- }
102
-
103
- return {
104
- pathname: locationValue.pathname,
105
- search: normalizeUrlPart(
106
- typeof locationValue.search === 'string'
107
- ? locationValue.search
108
- : locationValue.searchStr,
109
- '?',
110
- ),
111
- hash: normalizeUrlPart(locationValue.hash, '#'),
112
- };
113
- };
114
-
115
- const getWindowLocation = (): I18nRouterLocation | null => {
116
- if (!isBrowser()) {
117
- return null;
118
- }
119
-
120
- return {
121
- pathname: window.location.pathname,
122
- search: window.location.search,
123
- hash: window.location.hash,
41
+ /** Convert an already resolved target to the selected router's own props. */
42
+ createLinkProps?: (target: I18nRouterLinkTarget) => {
43
+ to: string;
44
+ [key: string]: unknown;
124
45
  };
125
- };
126
-
127
- const getRouterFramework = (
128
- runtimeContext: RuntimeContextWithRouter,
129
- internalContext: InternalRuntimeContextWithRouter,
130
- inReactRouter: boolean,
131
- ): I18nRouterFramework | undefined => {
132
- const framework =
133
- getRouterRuntimeState(internalContext)?.framework ||
134
- getRouterRuntimeState(runtimeContext)?.framework;
135
-
136
- if (framework) {
137
- return framework;
138
- }
139
-
140
- if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
141
- return 'tanstack';
142
- }
143
-
144
- if (
145
- internalContext.router?.useLocation ||
146
- internalContext.router?.useHref ||
147
- runtimeContext.router?.useLocation ||
148
- runtimeContext.router?.useHref
149
- ) {
150
- return 'react-router';
151
- }
152
-
153
- if (inReactRouter) {
154
- return 'react-router';
155
- }
156
-
157
- return undefined;
158
- };
159
-
160
- const getRouterInstance = (
161
- internalContext: InternalRuntimeContextWithRouter,
162
- contextRouter?: RouterInstance | null,
163
- ): RouterInstance | null => {
164
- if (contextRouter) {
165
- return contextRouter;
166
- }
167
-
168
- const router = getRouterRuntimeState(internalContext)?.instance;
169
- if (!router || typeof router !== 'object') {
170
- return null;
171
- }
172
- return router as RouterInstance;
173
- };
174
-
175
- const getRouterStateLocation = (
176
- internalContext: InternalRuntimeContextWithRouter,
177
- contextRouter?: RouterInstance | null,
178
- ): I18nRouterLocation | null => {
179
- const router = getRouterInstance(internalContext, contextRouter);
180
- return (
181
- normalizeLocation(router?.stores?.location?.get?.()) ||
182
- normalizeLocation(router?.state?.location)
183
- );
184
- };
185
-
186
- const getRouterParams = (
187
- internalContext: InternalRuntimeContextWithRouter,
188
- contextRouter?: RouterInstance | null,
189
- ): Record<string, string> => {
190
- const router = getRouterInstance(internalContext, contextRouter);
191
- const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
192
- if (!Array.isArray(matches)) {
193
- return {};
194
- }
195
-
196
- return matches.reduce<Record<string, string>>((params, match) => {
197
- if (match?.params) {
198
- Object.assign(params, match.params);
199
- }
200
- return params;
201
- }, {});
202
- };
46
+ }
203
47
 
204
- const getRouterSnapshot = (
205
- internalContext: InternalRuntimeContextWithRouter,
206
- contextRouter?: RouterInstance | null,
207
- ) => {
208
- const location = getRouterStateLocation(internalContext, contextRouter);
209
- const params = getRouterParams(internalContext, contextRouter);
210
- return JSON.stringify([
211
- location?.pathname ?? '',
212
- location?.search ?? '',
213
- location?.hash ?? '',
214
- Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
215
- ]);
216
- };
48
+ export const I18nNavigationProvider = ({
49
+ children,
50
+ value,
51
+ }: React.PropsWithChildren<{ value: I18nRouterAdapter }>) => (
52
+ <I18nNavigationContext.Provider value={value}>
53
+ {children}
54
+ </I18nNavigationContext.Provider>
55
+ );
56
+
57
+ interface NativeRouterApi {
58
+ useInRouterContext?: () => boolean;
59
+ useNavigate?: () => I18nRouterNavigate;
60
+ useLocation?: () => I18nRouterLocation;
61
+ useParams?: () => Record<string, string>;
62
+ Link?: I18nRouterLink;
63
+ }
217
64
 
218
- export const useI18nRouterAdapter = (): I18nRouterAdapter => {
219
- const runtimeContext = useContext(RuntimeContext) as RuntimeContextWithRouter;
220
- const internalContext = useContext(
221
- InternalRuntimeContext,
222
- ) as InternalRuntimeContextWithRouter;
223
- const routerApi = internalContext.router || runtimeContext.router;
224
- const inReactRouter = routerApi?.useInRouterContext?.() ?? false;
225
- const reactRouterNavigate = inReactRouter
226
- ? (routerApi?.useNavigate?.() ?? null)
227
- : null;
228
- const reactRouterLocation = inReactRouter
65
+ /** Read native router hooks only when their own provider is present. */
66
+ export const useNativeI18nRouterAdapter = (): I18nRouterAdapter => {
67
+ const runtimeContext = useContext(RuntimeContext);
68
+ const internalContext = useContext(InternalRuntimeContext);
69
+ const routerApi = (internalContext.router || runtimeContext.router) as
70
+ | NativeRouterApi
71
+ | undefined;
72
+ const hasRouter = routerApi?.useInRouterContext?.() ?? false;
73
+ const navigate = hasRouter ? (routerApi?.useNavigate?.() ?? null) : null;
74
+ const routerLocation = hasRouter
229
75
  ? (routerApi?.useLocation?.() ?? null)
230
76
  : null;
231
- const reactRouterParams = inReactRouter
232
- ? (routerApi?.useParams?.() ?? {})
233
- : {};
234
- const framework = getRouterFramework(
235
- runtimeContext,
236
- internalContext,
237
- inReactRouter,
238
- );
239
- const contextUseRouter =
240
- !inReactRouter && framework === 'tanstack'
241
- ? internalContext.router?.useRouter || runtimeContext.router?.useRouter
242
- : undefined;
243
- const contextRouter = contextUseRouter
244
- ? (contextUseRouter({ warn: false }) as RouterInstance | null)
245
- : null;
246
- const hasRouter =
247
- framework === 'tanstack' ||
248
- framework === 'react-router' ||
249
- Boolean(reactRouterNavigate);
250
-
251
- const subscribeToRouter = useCallback(
252
- (update: () => void) => {
253
- const router = getRouterInstance(internalContext, contextRouter);
254
- if (!router) {
255
- return () => undefined;
256
- }
257
-
258
- const unsubscribers: Array<() => void> = [];
259
-
260
- if (
261
- framework === 'react-router' &&
262
- !inReactRouter &&
263
- typeof router.subscribe === 'function'
264
- ) {
265
- const subscribe = router.subscribe as (
266
- this: RouterInstance,
267
- listener: () => void,
268
- ) => () => void;
269
- const unsubscribe = subscribe.call(router, update);
270
- if (typeof unsubscribe === 'function') {
271
- unsubscribers.push(unsubscribe);
272
- }
273
- }
274
-
275
- if (
276
- framework === 'tanstack' &&
277
- typeof router.stores?.location?.subscribe === 'function'
278
- ) {
279
- const unsubscribe = router.stores.location.subscribe(update);
280
- if (typeof unsubscribe === 'function') {
281
- unsubscribers.push(unsubscribe);
282
- }
283
- }
284
-
285
- if (framework === 'tanstack' && typeof router.subscribe === 'function') {
286
- const subscribe = router.subscribe as (
287
- this: RouterInstance,
288
- eventType: string,
289
- listener: () => void,
290
- ) => () => void;
291
- for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
292
- const unsubscribe = subscribe.call(router, eventType, update);
293
- if (typeof unsubscribe === 'function') {
294
- unsubscribers.push(unsubscribe);
295
- }
296
- }
297
- }
298
-
299
- return () => {
300
- for (const unsubscribe of unsubscribers) {
301
- unsubscribe();
302
- }
303
- };
304
- },
305
- [contextRouter, framework, inReactRouter, internalContext],
306
- );
307
- const getSnapshot = useCallback(
308
- () => getRouterSnapshot(internalContext, contextRouter),
309
- [contextRouter, internalContext],
310
- );
311
- useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
312
-
313
- const navigate = useCallback<I18nRouterNavigate>(
314
- (href, options) => {
315
- const router = getRouterInstance(internalContext, contextRouter);
316
- const activeFramework = getRouterFramework(
317
- runtimeContext,
318
- internalContext,
319
- inReactRouter,
320
- );
321
-
322
- if (activeFramework === 'tanstack') {
323
- if (typeof router?.navigate === 'function') {
324
- return router.navigate({
325
- to: href,
326
- replace: options?.replace,
327
- ...(options?.state === undefined ? {} : { state: options.state }),
328
- }) as void | Promise<void>;
329
- }
330
- throw new Error('TanStack router instance is not available.');
331
- }
332
-
333
- if (reactRouterNavigate) {
334
- return reactRouterNavigate(href, options);
335
- }
336
-
337
- if (activeFramework === 'react-router') {
338
- if (typeof router?.navigate === 'function') {
339
- return router.navigate(href, options) as void | Promise<void>;
340
- }
341
- throw new Error('React Router instance is not available.');
342
- }
343
- },
344
- [
345
- contextRouter,
346
- internalContext,
347
- inReactRouter,
348
- reactRouterNavigate,
349
- runtimeContext,
350
- ],
351
- );
352
-
77
+ const params = hasRouter ? (routerApi?.useParams?.() ?? {}) : {};
353
78
  const location =
354
- (reactRouterLocation
355
- ? normalizeLocation(reactRouterLocation)
356
- : getRouterStateLocation(internalContext, contextRouter)) ||
357
- getWindowLocation();
358
- const params = inReactRouter
359
- ? (reactRouterParams as Record<string, string>)
360
- : getRouterParams(internalContext, contextRouter);
361
- const Link =
362
- framework === 'tanstack'
363
- ? internalContext.router?.Link || runtimeContext.router?.Link || null
364
- : framework === 'react-router' || inReactRouter
365
- ? (routerApi?.Link ?? null)
366
- : null;
79
+ routerLocation ??
80
+ (isBrowser()
81
+ ? {
82
+ pathname: window.location.pathname,
83
+ search: window.location.search,
84
+ hash: window.location.hash,
85
+ }
86
+ : null);
367
87
 
368
88
  return {
369
- framework,
89
+ framework: hasRouter ? 'react-router' : undefined,
370
90
  hasRouter,
91
+ navigate,
371
92
  location,
372
- navigate: hasRouter ? navigate : null,
373
- Link,
374
93
  params,
94
+ Link: hasRouter ? (routerApi?.Link ?? null) : null,
375
95
  };
376
96
  };
97
+
98
+ export const useI18nRouterAdapter = (): I18nRouterAdapter => {
99
+ const provided = useContext(I18nNavigationContext);
100
+ const native = useNativeI18nRouterAdapter();
101
+ return provided ?? native;
102
+ };
@@ -0,0 +1,8 @@
1
+ // Upstream-owned runtime modules take the i18n contexts from this fork-owned
2
+ // module, which reaches them through the shared
3
+ // `@modern-js/plugin-i18n/runtime/contexts` request. That keeps the fork
4
+ // import edge out of the upstream-owned files.
5
+ export {
6
+ ModernI18nContext,
7
+ ReactI18nextProviderContext,
8
+ } from '@modern-js/plugin-i18n/runtime/contexts';
@@ -1,10 +1,10 @@
1
- import {
2
- type LocalisedUrlsOption,
3
- localiseTargetPathname,
4
- shouldSkipLocaleRedirect,
5
- } from '@modern-js/i18n-runtime-extensions';
6
1
  import { isBrowser } from '@modern-js/runtime';
7
2
  import { getGlobalBasename } from '@modern-js/runtime/context';
3
+ import { splitUrlTarget } from '@modern-js/runtime-utils/url';
4
+ import { isDefaultLocaleRedirectSkipPath } from '../shared/mappedUrlStrategy';
5
+ import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
6
+
7
+ export { splitUrlTarget } from '@modern-js/runtime-utils/url';
8
8
 
9
9
  // Structural parameter: hooks.ts passes a public-TRuntimeContext-based
10
10
  // context while core.tsx passes the internal one; both carry the request
@@ -47,24 +47,6 @@ export const getLanguageFromPath = (
47
47
  return fallbackLanguage;
48
48
  };
49
49
 
50
- /**
51
- * Split a link target into its pathname, search and hash parts without
52
- * relying on `new URL` (SSR-hot path; targets are relative).
53
- */
54
- export const splitUrlTarget = (
55
- target: string,
56
- ): { pathname: string; search: string; hash: string } => {
57
- const hashIndex = target.indexOf('#');
58
- const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
59
- const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
60
- const searchIndex = beforeHash.indexOf('?');
61
- const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
62
- const pathname =
63
- searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
64
-
65
- return { pathname, search, hash };
66
- };
67
-
68
50
  /**
69
51
  * Helper function to build localized URL
70
52
  * @param target - The language-agnostic target; may include `?search` and `#hash`
@@ -76,15 +58,23 @@ export const buildLocalizedUrl = (
76
58
  target: string,
77
59
  language: string,
78
60
  languages: string[],
79
- localisedUrls?: LocalisedUrlsOption,
61
+ urlStrategy?: I18nUrlStrategy,
80
62
  ): string => {
81
63
  const { pathname, search, hash } = splitUrlTarget(target);
82
- const localizedPathname = localiseTargetPathname(
83
- pathname,
84
- language,
85
- languages,
86
- localisedUrls,
87
- );
64
+ const strategy = asI18nUrlStrategy(urlStrategy);
65
+ if (strategy) {
66
+ return `${strategy.localizePathname(pathname, language, languages)}${search}${hash}`;
67
+ }
68
+ const segments = pathname.split('/').filter(Boolean);
69
+ if (
70
+ segments[0] &&
71
+ languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
72
+ ) {
73
+ segments[0] = language;
74
+ } else {
75
+ segments.unshift(language);
76
+ }
77
+ const localizedPathname = `/${segments.join('/')}`;
88
78
 
89
79
  return `${localizedPathname}${search}${hash}`;
90
80
  };
@@ -131,6 +121,34 @@ export const shouldIgnoreRedirect = (
131
121
  pathname: string,
132
122
  languages: string[],
133
123
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
124
+ urlStrategy?: I18nUrlStrategy,
134
125
  ): boolean => {
135
- return shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
126
+ // Same native exclusions as the server: federation artifacts are never
127
+ // locale-redirected, with or without a configured strategy.
128
+ if (isDefaultLocaleRedirectSkipPath(pathname, languages)) {
129
+ return true;
130
+ }
131
+ if (
132
+ asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(pathname, languages)
133
+ ) {
134
+ return true;
135
+ }
136
+ if (!ignoreRedirectRoutes) {
137
+ return false;
138
+ }
139
+ const segments = pathname.split('/').filter(Boolean);
140
+ if (
141
+ segments[0] &&
142
+ languages.some(item => item.toLowerCase() === segments[0].toLowerCase())
143
+ ) {
144
+ segments.shift();
145
+ }
146
+ const normalizedPath = `/${segments.join('/')}`;
147
+ return typeof ignoreRedirectRoutes === 'function'
148
+ ? ignoreRedirectRoutes(normalizedPath)
149
+ : ignoreRedirectRoutes.some(
150
+ pattern =>
151
+ normalizedPath === pattern ||
152
+ normalizedPath.startsWith(`${pattern}/`),
153
+ );
136
154
  };
@@ -0,0 +1,27 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
3
+ import { createMappedUrlStrategy } from './mappedUrlStrategy';
4
+
5
+ /**
6
+ * Mapped locale URLs work without any extra plugin. `localeDetection.localisedUrls`
7
+ * alone is enough: the server derives the URL policy from the map it is given.
8
+ * An explicit `resolveUrlStrategy` still wins, so an integration can replace the
9
+ * policy wholesale.
10
+ */
11
+ export const createDefaultUrlStrategy = (
12
+ localisedUrls?: LocalisedUrlsOption,
13
+ ): I18nUrlStrategy => createMappedUrlStrategy(localisedUrls);
14
+
15
+ /** A non-empty map is the only configuration that needs a derived policy. */
16
+ export const resolveDefaultUrlStrategy = (
17
+ localisedUrls: unknown,
18
+ ): I18nUrlStrategy | undefined => {
19
+ if (
20
+ !localisedUrls ||
21
+ typeof localisedUrls !== 'object' ||
22
+ Object.keys(localisedUrls).length === 0
23
+ ) {
24
+ return undefined;
25
+ }
26
+ return createDefaultUrlStrategy(localisedUrls as LocalisedUrlsOption);
27
+ };
@@ -1,12 +1,13 @@
1
- import { resolveLocalisedUrlsConfig } from '@modern-js/i18n-runtime-extensions';
2
1
  import * as honoPkg from '@modern-js/server-core/hono';
3
2
 
4
3
  const { languageDetector } = honoPkg;
5
4
 
6
5
  import type { Context, Next, ServerPlugin } from '@modern-js/server-runtime';
7
6
  import type { LocaleDetectionOptions } from '../shared/type';
7
+ import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
8
8
  import { getLocaleDetectionOptions } from '../shared/utils.js';
9
9
  import { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
10
+ import { resolveDefaultUrlStrategy } from './defaultUrlStrategy.js';
10
11
  import { convertToHonoLanguageDetectorOptions } from './detectorOptions.js';
11
12
  import {
12
13
  buildLocalizedUrl,
@@ -21,6 +22,7 @@ export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
21
22
  export interface I18nPluginOptions {
22
23
  localeDetection: LocaleDetectionOptions;
23
24
  staticRoutePrefixes: string[];
25
+ resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
24
26
  }
25
27
 
26
28
  export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
@@ -53,6 +55,10 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
53
55
  if (!options.localeDetection) {
54
56
  return;
55
57
  }
58
+ const entryLocaleDetection = getLocaleDetectionOptions(
59
+ entryName,
60
+ options.localeDetection,
61
+ );
56
62
  const {
57
63
  localePathRedirect,
58
64
  i18nextDetector = true,
@@ -60,8 +66,16 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
60
66
  fallbackLanguage = 'en',
61
67
  detection,
62
68
  ignoreRedirectRoutes,
63
- localisedUrls,
64
- } = getLocaleDetectionOptions(entryName, options.localeDetection);
69
+ } = entryLocaleDetection;
70
+ const legacyLocalisedUrls = Reflect.get(
71
+ entryLocaleDetection,
72
+ 'localisedUrls',
73
+ );
74
+ // `localeDetection.localisedUrls` is self-sufficient: when no explicit
75
+ // strategy is supplied the plugin derives one from the map itself.
76
+ const urlStrategy =
77
+ asI18nUrlStrategy(options.resolveUrlStrategy?.(entryName)) ??
78
+ resolveDefaultUrlStrategy(legacyLocalisedUrls);
65
79
  const staticRoutePrefixes = options.staticRoutePrefixes;
66
80
  const originUrlPath = route.urlPath;
67
81
  const urlPath = originUrlPath.endsWith('/')
@@ -87,6 +101,18 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
87
101
  return await next();
88
102
  }
89
103
 
104
+ if (
105
+ shouldIgnoreRedirect(
106
+ pathname,
107
+ urlPath,
108
+ ignoreRedirectRoutes,
109
+ urlStrategy,
110
+ languages,
111
+ )
112
+ ) {
113
+ return await next();
114
+ }
115
+
90
116
  // For static resource requests, skip language detection
91
117
  if (
92
118
  isStaticResourceRequest(
@@ -138,7 +164,13 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
138
164
 
139
165
  // Check if this route should ignore automatic redirect
140
166
  if (
141
- shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)
167
+ shouldIgnoreRedirect(
168
+ pathname,
169
+ urlPath,
170
+ ignoreRedirectRoutes,
171
+ urlStrategy,
172
+ languages,
173
+ )
142
174
  ) {
143
175
  return await next();
144
176
  }
@@ -168,19 +200,17 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
168
200
  originUrlPath,
169
201
  targetLanguage,
170
202
  languages,
171
- localisedUrls,
203
+ urlStrategy,
172
204
  );
173
205
  return createLocaleRedirectResponse(localizedUrl);
174
206
  }
175
- const localisedUrlsConfig =
176
- resolveLocalisedUrlsConfig(localisedUrls);
177
- if (localisedUrlsConfig.enabled) {
207
+ if (urlStrategy) {
178
208
  const expectedUrl = buildLocalizedUrl(
179
209
  c.req,
180
210
  originUrlPath,
181
211
  language,
182
212
  languages,
183
- localisedUrls,
213
+ urlStrategy,
184
214
  );
185
215
  if (expectedUrl !== `${pathname}${url.search}${url.hash}`) {
186
216
  return createLocaleRedirectResponse(expectedUrl);