@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6

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 (79) hide show
  1. package/dist/cjs/cli/index.js +0 -22
  2. package/dist/cjs/runtime/Link.js +19 -32
  3. package/dist/cjs/runtime/context.js +26 -61
  4. package/dist/cjs/runtime/contextHelpers.js +5 -5
  5. package/dist/cjs/runtime/core.js +17 -14
  6. package/dist/cjs/runtime/hooks.js +7 -8
  7. package/dist/cjs/runtime/localizedPaths.js +11 -9
  8. package/dist/cjs/runtime/providerComposition.js +37 -13
  9. package/dist/cjs/runtime/routerAdapter.js +35 -140
  10. package/dist/cjs/runtime/utils.js +17 -19
  11. package/dist/cjs/server/index.js +10 -7
  12. package/dist/cjs/server/redirectPolicy.js +36 -31
  13. package/dist/cjs/shared/urlStrategy.js +18 -0
  14. package/dist/esm/cli/index.mjs +0 -22
  15. package/dist/esm/runtime/Link.mjs +19 -32
  16. package/dist/esm/runtime/context.mjs +18 -62
  17. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  18. package/dist/esm/runtime/core.mjs +8 -5
  19. package/dist/esm/runtime/hooks.mjs +7 -8
  20. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  21. package/dist/esm/runtime/providerComposition.mjs +38 -14
  22. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  23. package/dist/esm/runtime/utils.mjs +15 -17
  24. package/dist/esm/server/index.mjs +10 -7
  25. package/dist/esm/server/redirectPolicy.mjs +29 -24
  26. package/dist/esm/shared/urlStrategy.mjs +0 -0
  27. package/dist/esm-node/cli/index.mjs +0 -22
  28. package/dist/esm-node/runtime/Link.mjs +19 -32
  29. package/dist/esm-node/runtime/context.mjs +18 -62
  30. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  31. package/dist/esm-node/runtime/core.mjs +8 -5
  32. package/dist/esm-node/runtime/hooks.mjs +7 -8
  33. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  34. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  35. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  36. package/dist/esm-node/runtime/utils.mjs +15 -17
  37. package/dist/esm-node/server/index.mjs +10 -7
  38. package/dist/esm-node/server/redirectPolicy.mjs +29 -24
  39. package/dist/esm-node/shared/urlStrategy.mjs +1 -0
  40. package/dist/types/runtime/context.d.ts +8 -11
  41. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  42. package/dist/types/runtime/core.d.ts +11 -2
  43. package/dist/types/runtime/hooks.d.ts +4 -5
  44. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  45. package/dist/types/runtime/providerComposition.d.ts +13 -1
  46. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  47. package/dist/types/runtime/utils.d.ts +4 -12
  48. package/dist/types/server/index.d.ts +2 -0
  49. package/dist/types/server/redirectPolicy.d.ts +5 -13
  50. package/dist/types/shared/type.d.ts +0 -14
  51. package/dist/types/shared/urlStrategy.d.ts +7 -0
  52. package/package.json +21 -11
  53. package/rstest.config.mts +2 -8
  54. package/src/cli/index.ts +1 -43
  55. package/src/runtime/Link.tsx +20 -40
  56. package/src/runtime/context.tsx +34 -133
  57. package/src/runtime/contextHelpers.ts +7 -5
  58. package/src/runtime/core.tsx +26 -6
  59. package/src/runtime/hooks.ts +14 -9
  60. package/src/runtime/localizedPaths.ts +24 -19
  61. package/src/runtime/providerComposition.tsx +71 -23
  62. package/src/runtime/routerAdapter.tsx +73 -338
  63. package/src/runtime/utils.ts +40 -31
  64. package/src/server/index.ts +44 -9
  65. package/src/server/redirectPolicy.ts +70 -70
  66. package/src/shared/type.ts +0 -14
  67. package/src/shared/urlStrategy.ts +11 -0
  68. package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
  69. package/tests/i18nUtils.test.ts +31 -65
  70. package/tests/link.test.tsx +13 -511
  71. package/tests/linkTypes.test.ts +12 -3
  72. package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
  73. package/tests/localisedUrls.test.ts +7 -216
  74. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  75. package/tests/redirectPolicy.test.ts +39 -30
  76. package/tests/routerAdapter.test.tsx +95 -1055
  77. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  78. package/tests/federatedI18nBoundary.test.tsx +0 -132
  79. package/tests/peerCohort.test.ts +0 -25
@@ -1,9 +1,11 @@
1
1
  import type { ServerPlugin } from '@modern-js/server-runtime';
2
2
  import type { LocaleDetectionOptions } from '../shared/type.js';
3
+ import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
3
4
  export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
4
5
  export interface I18nPluginOptions {
5
6
  localeDetection: LocaleDetectionOptions;
6
7
  staticRoutePrefixes: string[];
8
+ resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
7
9
  }
8
10
  export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
9
11
  export default i18nServerPlugin;
@@ -1,22 +1,14 @@
1
- import type { LocaleDetectionOptions } from '../shared/type.js';
1
+ import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
2
2
  interface LocaleRedirectRequest {
3
3
  url: string;
4
- header: () => {
4
+ header?: () => {
5
5
  host?: string;
6
6
  };
7
7
  }
8
- /**
9
- * Check if pathname should ignore automatic locale redirect.
10
- */
11
- export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
12
- /**
13
- * Check if pathname is a static or federation resource request.
14
- *
15
- * This includes configured staticRoutePrefixes, the shared default skip policy
16
- * from ADR-0002, and language-prefixed variants such as /en/static/app.js.
17
- */
8
+ export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean), urlStrategy?: I18nUrlStrategy, languages?: string[]) => boolean;
9
+ /** Native configured/static exclusions remain in effect for every strategy. */
18
10
  export declare const isStaticResourceRequest: (pathname: string, staticRoutePrefixes: string[], languages?: string[]) => boolean;
19
11
  export declare const getLanguageFromPath: (req: LocaleRedirectRequest, urlPath: string, languages: string[]) => string | null;
20
- export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[], localisedUrls?: LocaleDetectionOptions['localisedUrls']) => string;
12
+ export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[], urlStrategy?: I18nUrlStrategy) => string;
21
13
  export declare const createLocaleRedirectResponse: (location: string) => Response;
22
14
  export {};
@@ -1,4 +1,3 @@
1
- import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
1
  import type { LanguageDetectorOptions, Resources } from '../runtime/i18n/instance.js';
3
2
  export interface BaseLocaleDetectionOptions {
4
3
  localePathRedirect?: boolean;
@@ -7,19 +6,6 @@ export interface BaseLocaleDetectionOptions {
7
6
  fallbackLanguage?: string;
8
7
  detection?: LanguageDetectorOptions;
9
8
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
10
- /**
11
- * Enables localised pathnames in addition to the locale prefix.
12
- *
13
- * - non-empty object: map canonical route paths to every configured
14
- * language; route generation then validates that every localisable route
15
- * path has entries for all configured languages.
16
- * - absent / `false` / `true` / empty object: keep only locale-prefix
17
- * behavior (`/en/about`).
18
- *
19
- * Strictly opt-in: without a map, `localePathRedirect` + `languages` behave
20
- * exactly like upstream Modern.js.
21
- */
22
- localisedUrls?: LocalisedUrlsOption;
23
9
  }
24
10
  export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
25
11
  localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
@@ -0,0 +1,7 @@
1
+ /** Per-plugin URL policy. Search and hash remain owned by native URL handling. */
2
+ export interface I18nUrlStrategy {
3
+ localizePathname(pathname: string, language: string, languages: readonly string[]): string;
4
+ canonicalPathname(pathname: string, languages: readonly string[]): string;
5
+ /** Adds exclusions; it cannot enable redirects excluded by native rules. */
6
+ shouldSkipRedirect?(pathname: string, languages: readonly string[]): boolean;
7
+ }
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.9.0-ultramodern.4",
20
+ "version": "3.9.0-ultramodern.6",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -80,6 +80,14 @@
80
80
  "./i18n": {
81
81
  "types": "./dist/types/runtime/i18n/index.d.ts",
82
82
  "default": "./dist/esm/runtime/i18n/index.mjs"
83
+ },
84
+ "./config": {
85
+ "types": "./dist/types/shared/utils.d.ts",
86
+ "node": {
87
+ "import": "./dist/esm-node/shared/utils.mjs",
88
+ "require": "./dist/cjs/shared/utils.js"
89
+ },
90
+ "default": "./dist/esm/shared/utils.mjs"
83
91
  }
84
92
  },
85
93
  "typesVersions": {
@@ -107,17 +115,19 @@
107
115
  ],
108
116
  "i18n": [
109
117
  "./dist/types/runtime/i18n/index.d.ts"
118
+ ],
119
+ "config": [
120
+ "./dist/types/shared/utils.d.ts"
110
121
  ]
111
122
  }
112
123
  },
113
124
  "dependencies": {
114
- "@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.4",
115
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.4",
116
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.4",
117
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.4",
118
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.4",
119
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.4",
120
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.4",
125
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.6",
126
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.6",
127
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.6",
128
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.6",
129
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.6",
130
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.6",
121
131
  "@swc/helpers": "^0.5.23",
122
132
  "i18next-browser-languagedetector": "^8.2.1",
123
133
  "i18next-chained-backend": "^5.0.6",
@@ -126,7 +136,7 @@
126
136
  "i18next-http-middleware": "^3.9.8"
127
137
  },
128
138
  "peerDependencies": {
129
- "@modern-js/runtime": "3.9.0-ultramodern.4",
139
+ "@modern-js/runtime": "3.9.0-ultramodern.6",
130
140
  "i18next": ">=25.7.4",
131
141
  "react": "^19.2.8",
132
142
  "react-dom": "^19.2.8",
@@ -141,8 +151,8 @@
141
151
  }
142
152
  },
143
153
  "devDependencies": {
144
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.4",
145
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.4",
154
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.6",
155
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.6",
146
156
  "@rslib/core": "1.0.0",
147
157
  "@types/node": "^26.4.1",
148
158
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
package/rstest.config.mts CHANGED
@@ -29,24 +29,18 @@ export default {
29
29
  include: [
30
30
  'tests/i18nUtils.test.ts',
31
31
  'tests/localisedUrls.test.ts',
32
- 'tests/localisedUrlRewriteMatrix.fork.test.ts',
33
32
  'tests/linkTypes.test.ts',
34
33
  'tests/backendDefaults.test.ts',
35
34
  'tests/redirectPolicy.test.ts',
36
- 'tests/peerCohort.test.ts',
37
35
  'tests/reactI18nextRuntimeBoundary.test.ts',
38
- 'tests/federatedI18nBoundary.test.tsx',
36
+ 'tests/federatedI18nBoundaryResolver.test.tsx',
39
37
  ],
40
38
  extends: commonConfig,
41
39
  }),
42
40
  withTestPreset({
43
41
  name: 'plugin-i18n-client',
44
42
  testEnvironment: 'happy-dom',
45
- include: [
46
- 'tests/routerAdapter.test.tsx',
47
- 'tests/link.test.tsx',
48
- 'tests/federatedI18nBoundary.client.test.tsx',
49
- ],
43
+ include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
50
44
  extends: commonConfig,
51
45
  }),
52
46
  ],
package/src/cli/index.ts CHANGED
@@ -1,16 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import type { AppTools, CliPlugin } from '@modern-js/app-tools';
4
- import {
5
- applyLocalisedUrlsToRoutes,
6
- resolveLocalisedUrlsConfig,
7
- } from '@modern-js/i18n-runtime-extensions';
8
4
  import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
9
- import type {
10
- Entrypoint,
11
- NestedRouteForCli,
12
- PageRoute,
13
- } from '@modern-js/types';
5
+ import type { Entrypoint } from '@modern-js/types';
14
6
  import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
15
7
  import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
16
8
  import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
@@ -144,40 +136,6 @@ export const i18nPlugin = (
144
136
  };
145
137
  });
146
138
 
147
- api.modifyFileSystemRoutes(({ entrypoint, routes }) => {
148
- if (!localeDetection) {
149
- return { entrypoint, routes };
150
- }
151
-
152
- const localeDetectionOptions = getLocaleDetectionOptions(
153
- entrypoint.entryName,
154
- localeDetection,
155
- );
156
- const {
157
- localePathRedirect,
158
- languages = [],
159
- localisedUrls,
160
- } = localeDetectionOptions;
161
-
162
- if (!localePathRedirect || languages.length === 0) {
163
- return { entrypoint, routes };
164
- }
165
-
166
- const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
167
- if (!localisedUrlsConfig.enabled) {
168
- return { entrypoint, routes };
169
- }
170
-
171
- return {
172
- entrypoint,
173
- routes: applyLocalisedUrlsToRoutes(
174
- routes as (NestedRouteForCli | PageRoute)[],
175
- languages,
176
- localisedUrlsConfig.map,
177
- ) as (NestedRouteForCli | PageRoute)[],
178
- };
179
- });
180
-
181
139
  api._internalServerPlugins(({ plugins }) => {
182
140
  const { serverRoutes, metaName } = api.getAppContext();
183
141
  const normalizedConfig = api.getNormalizedConfig();
@@ -104,11 +104,11 @@ export const Link = <TTo extends string = string>(
104
104
  } = props as LinkBaseProps & { to: string; params?: LinkParams };
105
105
 
106
106
  const adapter = useI18nRouterAdapter();
107
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
107
+ const { language, supportedLanguages, urlStrategy } = useModernI18n();
108
108
 
109
109
  const config: LocalizedPathsConfig = {
110
110
  languages: supportedLanguages,
111
- localisedUrls,
111
+ urlStrategy,
112
112
  };
113
113
 
114
114
  const isExternal = EXTERNAL_TARGET_RE.test(to);
@@ -139,7 +139,7 @@ export const Link = <TTo extends string = string>(
139
139
  interpolated,
140
140
  language,
141
141
  supportedLanguages,
142
- localisedUrls,
142
+ urlStrategy,
143
143
  );
144
144
  const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
145
145
  const { searchString, searchObject } = normalizeSearch(
@@ -164,7 +164,7 @@ export const Link = <TTo extends string = string>(
164
164
  isBareHash,
165
165
  language,
166
166
  supportedLanguages,
167
- localisedUrls,
167
+ urlStrategy,
168
168
  ]);
169
169
 
170
170
  const isActive = useMemo(() => {
@@ -190,7 +190,7 @@ export const Link = <TTo extends string = string>(
190
190
  adapter.location,
191
191
  activeOptions?.exact,
192
192
  supportedLanguages,
193
- localisedUrls,
193
+ urlStrategy,
194
194
  ]);
195
195
 
196
196
  const resolvedActiveProps = splitActiveProps(isActive, activeProps);
@@ -214,7 +214,7 @@ export const Link = <TTo extends string = string>(
214
214
  );
215
215
  }
216
216
 
217
- const { Link: RouterLink, hasRouter, framework } = adapter;
217
+ const { Link: RouterLink, hasRouter } = adapter;
218
218
 
219
219
  if (!hasRouter || !RouterLink) {
220
220
  const { replace: _replace, ...anchorProps } = rest;
@@ -252,43 +252,23 @@ export const Link = <TTo extends string = string>(
252
252
  ...(activeStyle as React.CSSProperties | undefined),
253
253
  };
254
254
 
255
- if (framework === 'tanstack') {
256
- // TanStack's prop is `preload`; map our react-router-flavored `prefetch`
257
- // onto it (`'none'` -> `false`). An explicit native `preload` wins.
258
- const tanstackPreload =
259
- preload !== undefined
260
- ? preload
261
- : prefetch === undefined
262
- ? undefined
263
- : prefetch === 'none'
264
- ? false
265
- : prefetch;
266
-
267
- // Pass hash/search natively: string-concatenated targets silently break
268
- // TanStack navigation.
269
- return (
270
- <RouterLink
271
- to={target.localizedPathname}
272
- {...(target.searchObject ? { search: target.searchObject } : {})}
273
- {...(target.hash ? { hash: target.hash } : {})}
274
- {...(hashScrollIntoView === undefined ? {} : { hashScrollIntoView })}
275
- {...(tanstackPreload === undefined ? {} : { preload: tanstackPreload })}
276
- {...rest}
277
- {...activeRest}
278
- {...activeAttributes}
279
- className={mergedClassName}
280
- style={mergedStyle}
281
- >
282
- {children}
283
- </RouterLink>
284
- );
285
- }
255
+ const routerProps = adapter.createLinkProps?.({
256
+ pathname: target.localizedPathname,
257
+ href: target.href,
258
+ search: target.searchObject,
259
+ hash: target.hash,
260
+ hashScrollIntoView,
261
+ prefetch,
262
+ preload,
263
+ }) ?? {
264
+ to: target.href,
265
+ ...(prefetch === undefined ? {} : { prefetch }),
266
+ ...(preload === undefined ? {} : { preload }),
267
+ };
286
268
 
287
269
  return (
288
270
  <RouterLink
289
- to={target.href}
290
- {...(prefetch === undefined ? {} : { prefetch })}
291
- {...(preload === undefined ? {} : { preload })}
271
+ {...routerProps}
292
272
  {...rest}
293
273
  {...activeRest}
294
274
  {...activeAttributes}
@@ -1,4 +1,4 @@
1
- import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
1
+ import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
2
2
  import type { ComponentType, FC, ReactNode } from 'react';
3
3
  import {
4
4
  createContext,
@@ -7,6 +7,7 @@ import {
7
7
  useEffect,
8
8
  useMemo,
9
9
  } from 'react';
10
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
10
11
  import {
11
12
  changeModernI18nLanguage,
12
13
  getPathLanguage,
@@ -16,10 +17,12 @@ import {
16
17
  } from './contextHelpers';
17
18
  import type { I18nInstance } from './i18n';
18
19
  import type { Resources } from './i18n/instance';
19
- import { getActualI18nextInstance } from './i18n/instance';
20
+
20
21
  import { useI18nRouterAdapter } from './routerAdapter';
21
22
 
22
- interface ModernI18nContextValue {
23
+ export { getActualI18nextInstance } from './i18n/instance';
24
+
25
+ export interface ModernI18nContextValue {
23
26
  language: string;
24
27
  i18nInstance: I18nInstance;
25
28
  // Plugin configuration for useModernI18n hook
@@ -27,7 +30,7 @@ interface ModernI18nContextValue {
27
30
  languages?: string[];
28
31
  localePathRedirect?: boolean;
29
32
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
30
- localisedUrls?: LocalisedUrlsOption;
33
+ urlStrategy?: I18nUrlStrategy;
31
34
  // Callback to update language in context
32
35
  updateLanguage?: (newLang: string) => void;
33
36
  synchronizeLanguage?: (newLang: string) => void;
@@ -50,16 +53,15 @@ const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
50
53
  return globalStore[key];
51
54
  };
52
55
 
53
- const ModernI18nContext = getGlobalContext<ModernI18nContextValue | null>(
54
- modernI18nContextKey,
55
- null,
56
- );
57
- const ReactI18nextProviderContext = getGlobalContext<ComponentType<any> | null>(
58
- reactI18nextProviderContextKey,
59
- null,
60
- );
56
+ export const ModernI18nContext =
57
+ getGlobalContext<ModernI18nContextValue | null>(modernI18nContextKey, null);
58
+ export const ReactI18nextProviderContext =
59
+ getGlobalContext<ComponentType<any> | null>(
60
+ reactI18nextProviderContextKey,
61
+ null,
62
+ );
61
63
 
62
- interface ModernI18nProviderProps {
64
+ export interface ModernI18nProviderProps {
63
65
  children: ReactNode;
64
66
  i18nextProvider?: ComponentType<any> | null;
65
67
  value: ModernI18nContextValue;
@@ -92,123 +94,22 @@ export interface FederatedI18nBoundaryProps {
92
94
  supportedLanguages?: string[];
93
95
  }
94
96
 
95
- /**
96
- * Keeps a federated surface's translation resources inside its delivery unit.
97
- * The host supplies only the active language; the remote owns and versions the
98
- * resources used below this boundary.
99
- */
100
- export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
101
- children,
102
- defaultNamespace,
103
- fallbackLanguage,
104
- resources,
105
- supportedLanguages,
106
- }) => {
107
- const parent = useContext(ModernI18nContext);
108
- const I18nextProvider = useContext(ReactI18nextProviderContext);
109
- if (!parent) {
110
- throw new Error(
111
- 'FederatedI18nBoundary must be used within ModernI18nProvider',
112
- );
113
- }
114
-
115
- const languages =
116
- supportedLanguages ?? parent.languages ?? Object.keys(resources);
117
- const scopedInstance = useMemo(() => {
118
- const parentInstance = getActualI18nextInstance(parent.i18nInstance);
119
- const clone = parentInstance.cloneInstance?.({
120
- defaultNS: defaultNamespace,
121
- fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
122
- forkResourceStore: true,
123
- initImmediate: false,
124
- lng: parent.language,
125
- ns: [defaultNamespace],
126
- resources,
127
- supportedLngs: languages,
128
- });
129
- if (!clone) {
130
- throw new Error(
131
- 'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
132
- );
133
- }
134
- const resourceStore = clone.store;
135
- const parentResourceStore = parentInstance.store;
136
- const sharesNestedResourceState = Object.entries(
137
- resourceStore?.data ?? {},
138
- ).some(([language, namespaces]) => {
139
- const parentNamespaces = parentResourceStore?.data?.[language];
140
- return (
141
- namespaces === parentNamespaces ||
142
- Object.entries(namespaces).some(
143
- ([namespace, resource]) =>
144
- resource !== null &&
145
- typeof resource === 'object' &&
146
- resource === parentNamespaces?.[namespace],
147
- )
148
- );
149
- });
150
- if (
151
- clone === parentInstance ||
152
- resourceStore === parentResourceStore ||
153
- resourceStore?.data === parentResourceStore?.data ||
154
- sharesNestedResourceState
155
- ) {
156
- throw new Error(
157
- 'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
158
- );
159
- }
160
- if (
161
- !resourceStore?.addResourceBundle ||
162
- !clone.removeResourceBundle ||
163
- !resourceStore.data
164
- ) {
165
- throw new Error(
166
- 'FederatedI18nBoundary requires an isolated mutable i18n resource store',
167
- );
168
- }
169
- for (const [language, namespaces] of Object.entries(resourceStore.data)) {
170
- for (const namespace of Object.keys(namespaces)) {
171
- clone.removeResourceBundle(language, namespace);
172
- }
173
- }
174
- for (const [language, namespaces] of Object.entries(resources)) {
175
- for (const [namespace, resource] of Object.entries(namespaces)) {
176
- resourceStore.addResourceBundle(
177
- language,
178
- namespace,
179
- resource as Record<string, string>,
180
- true,
181
- true,
182
- );
183
- }
184
- }
185
- return clone;
186
- }, [
187
- defaultNamespace,
188
- fallbackLanguage,
189
- languages,
190
- parent.i18nInstance,
191
- parent.language,
192
- resources,
193
- ]);
194
- const value = useMemo(
195
- () => ({
196
- ...parent,
197
- i18nInstance: scopedInstance,
198
- language: parent.language,
199
- languages,
200
- }),
201
- [languages, parent, scopedInstance],
97
+ const MissingFederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = () => {
98
+ throw new Error(
99
+ 'FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin',
202
100
  );
101
+ };
203
102
 
204
- const scopedContent = (
205
- <ModernI18nProvider value={value}>{children}</ModernI18nProvider>
206
- );
207
- return I18nextProvider ? (
208
- <I18nextProvider i18n={scopedInstance}>{scopedContent}</I18nextProvider>
209
- ) : (
210
- scopedContent
211
- );
103
+ export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = props => {
104
+ const resolveComponent = useContext(RuntimeComponentResolverContext);
105
+ const Boundary =
106
+ resolveComponent?.(MissingFederatedI18nBoundary, {
107
+ name: 'i18n.FederatedI18nBoundary',
108
+ }) ?? MissingFederatedI18nBoundary;
109
+ if (Boundary === FederatedI18nBoundary) {
110
+ throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
111
+ }
112
+ return <Boundary {...props} />;
212
113
  };
213
114
 
214
115
  export interface UseModernI18nReturn<
@@ -219,7 +120,7 @@ export interface UseModernI18nReturn<
219
120
  t: (key: string | string[], ...args: any[]) => string;
220
121
  i18nInstance: TInstance;
221
122
  supportedLanguages: string[];
222
- localisedUrls?: LocalisedUrlsOption;
123
+ urlStrategy?: I18nUrlStrategy;
223
124
  isLanguageSupported: (lang: string) => boolean;
224
125
  // Indicates whether translation resources for current language are ready
225
126
  isResourcesReady: boolean;
@@ -258,7 +159,7 @@ export const useModernI18n = <
258
159
  languages,
259
160
  localePathRedirect,
260
161
  ignoreRedirectRoutes,
261
- localisedUrls,
162
+ urlStrategy,
262
163
  updateLanguage,
263
164
  synchronizeLanguage,
264
165
  } = context;
@@ -295,7 +196,7 @@ export const useModernI18n = <
295
196
  updateLanguage,
296
197
  localePathRedirect,
297
198
  ignoreRedirectRoutes,
298
- localisedUrls,
199
+ urlStrategy,
299
200
  languages,
300
201
  hasRouter,
301
202
  navigate,
@@ -306,7 +207,7 @@ export const useModernI18n = <
306
207
  updateLanguage,
307
208
  localePathRedirect,
308
209
  ignoreRedirectRoutes,
309
- localisedUrls,
210
+ urlStrategy,
310
211
  languages,
311
212
  hasRouter,
312
213
  navigate,
@@ -341,7 +242,7 @@ export const useModernI18n = <
341
242
  // narrows to the concrete instance type via the TInstance type argument.
342
243
  i18nInstance: i18nInstance as TInstance,
343
244
  supportedLanguages: languages || [],
344
- localisedUrls,
245
+ urlStrategy,
345
246
  isLanguageSupported,
346
247
  isResourcesReady,
347
248
  };
@@ -1,5 +1,5 @@
1
- import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
1
  import { isBrowser } from '@modern-js/runtime';
2
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
3
3
  import type { I18nInstance } from './i18n';
4
4
  import type { SdkBackend } from './i18n/backend/sdk-backend';
5
5
  import { cacheUserLanguage } from './i18n/detection';
@@ -66,7 +66,7 @@ interface ChangeModernI18nLanguageOptions {
66
66
  updateLanguage?: (newLang: string) => void;
67
67
  localePathRedirect?: boolean;
68
68
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
69
- localisedUrls?: LocalisedUrlsOption;
69
+ urlStrategy?: I18nUrlStrategy;
70
70
  languages?: string[];
71
71
  hasRouter: boolean;
72
72
  navigate?: Navigate | null;
@@ -82,7 +82,7 @@ export async function changeModernI18nLanguage(
82
82
  updateLanguage,
83
83
  localePathRedirect,
84
84
  ignoreRedirectRoutes,
85
- localisedUrls,
85
+ urlStrategy,
86
86
  languages,
87
87
  hasRouter,
88
88
  navigate,
@@ -123,13 +123,14 @@ export async function changeModernI18nLanguage(
123
123
  relativePath,
124
124
  languages || [],
125
125
  ignoreRedirectRoutes,
126
+ urlStrategy,
126
127
  )
127
128
  ) {
128
129
  const newPath = buildLocalizedUrl(
129
130
  relativePath,
130
131
  newLang,
131
132
  languages || [],
132
- localisedUrls,
133
+ urlStrategy,
133
134
  );
134
135
  const newUrl = entryPath + newPath + location.search + location.hash;
135
136
 
@@ -155,13 +156,14 @@ export async function changeModernI18nLanguage(
155
156
  relativePath,
156
157
  languages || [],
157
158
  ignoreRedirectRoutes,
159
+ urlStrategy,
158
160
  )
159
161
  ) {
160
162
  const newPath = buildLocalizedUrl(
161
163
  relativePath,
162
164
  newLang,
163
165
  languages || [],
164
- localisedUrls,
166
+ urlStrategy,
165
167
  );
166
168
  const newUrl =
167
169
  entryPath + newPath + window.location.search + window.location.hash;