@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
@@ -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,12 +1,7 @@
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
- import {
4
- createContext,
5
- useCallback,
6
- useContext,
7
- useEffect,
8
- useMemo,
9
- } from 'react';
3
+ import { useCallback, useContext, useEffect, useMemo } from 'react';
4
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
10
5
  import {
11
6
  changeModernI18nLanguage,
12
7
  getPathLanguage,
@@ -16,10 +11,16 @@ import {
16
11
  } from './contextHelpers';
17
12
  import type { I18nInstance } from './i18n';
18
13
  import type { Resources } from './i18n/instance';
19
- import { getActualI18nextInstance } from './i18n/instance';
14
+
20
15
  import { useI18nRouterAdapter } from './routerAdapter';
16
+ import {
17
+ ModernI18nContext,
18
+ ReactI18nextProviderContext,
19
+ } from './sharedContexts';
20
+
21
+ export { getActualI18nextInstance } from './i18n/instance';
21
22
 
22
- interface ModernI18nContextValue {
23
+ export interface ModernI18nContextValue {
23
24
  language: string;
24
25
  i18nInstance: I18nInstance;
25
26
  // Plugin configuration for useModernI18n hook
@@ -27,39 +28,15 @@ interface ModernI18nContextValue {
27
28
  languages?: string[];
28
29
  localePathRedirect?: boolean;
29
30
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
30
- localisedUrls?: LocalisedUrlsOption;
31
+ urlStrategy?: I18nUrlStrategy;
31
32
  // Callback to update language in context
32
33
  updateLanguage?: (newLang: string) => void;
33
34
  synchronizeLanguage?: (newLang: string) => void;
34
35
  }
35
36
 
36
- const modernI18nContextKey = Symbol.for(
37
- '@modern-js/plugin-i18n/runtime/ModernI18nContext',
38
- );
39
- const reactI18nextProviderContextKey = Symbol.for(
40
- '@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext',
41
- );
42
-
43
- type GlobalContextStore<T> = typeof globalThis & {
44
- [key: symbol]: ReturnType<typeof createContext<T>> | undefined;
45
- };
46
-
47
- const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
48
- const globalStore = globalThis as GlobalContextStore<T>;
49
- globalStore[key] ??= createContext<T>(defaultValue);
50
- return globalStore[key];
51
- };
52
-
53
- const ModernI18nContext = getGlobalContext<ModernI18nContextValue | null>(
54
- modernI18nContextKey,
55
- null,
56
- );
57
- const ReactI18nextProviderContext = getGlobalContext<ComponentType<any> | null>(
58
- reactI18nextProviderContextKey,
59
- null,
60
- );
37
+ export { ModernI18nContext, ReactI18nextProviderContext };
61
38
 
62
- interface ModernI18nProviderProps {
39
+ export interface ModernI18nProviderProps {
63
40
  children: ReactNode;
64
41
  i18nextProvider?: ComponentType<any> | null;
65
42
  value: ModernI18nContextValue;
@@ -92,123 +69,22 @@ export interface FederatedI18nBoundaryProps {
92
69
  supportedLanguages?: string[];
93
70
  }
94
71
 
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],
72
+ const MissingFederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = () => {
73
+ throw new Error(
74
+ 'FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin',
202
75
  );
76
+ };
203
77
 
204
- const scopedContent = (
205
- <ModernI18nProvider value={value}>{children}</ModernI18nProvider>
206
- );
207
- return I18nextProvider ? (
208
- <I18nextProvider i18n={scopedInstance}>{scopedContent}</I18nextProvider>
209
- ) : (
210
- scopedContent
211
- );
78
+ export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = props => {
79
+ const resolveComponent = useContext(RuntimeComponentResolverContext);
80
+ const Boundary =
81
+ resolveComponent?.(MissingFederatedI18nBoundary, {
82
+ name: 'i18n.FederatedI18nBoundary',
83
+ }) ?? MissingFederatedI18nBoundary;
84
+ if (Boundary === FederatedI18nBoundary) {
85
+ throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
86
+ }
87
+ return <Boundary {...props} />;
212
88
  };
213
89
 
214
90
  export interface UseModernI18nReturn<
@@ -219,7 +95,7 @@ export interface UseModernI18nReturn<
219
95
  t: (key: string | string[], ...args: any[]) => string;
220
96
  i18nInstance: TInstance;
221
97
  supportedLanguages: string[];
222
- localisedUrls?: LocalisedUrlsOption;
98
+ urlStrategy?: I18nUrlStrategy;
223
99
  isLanguageSupported: (lang: string) => boolean;
224
100
  // Indicates whether translation resources for current language are ready
225
101
  isResourcesReady: boolean;
@@ -258,7 +134,7 @@ export const useModernI18n = <
258
134
  languages,
259
135
  localePathRedirect,
260
136
  ignoreRedirectRoutes,
261
- localisedUrls,
137
+ urlStrategy,
262
138
  updateLanguage,
263
139
  synchronizeLanguage,
264
140
  } = context;
@@ -295,7 +171,7 @@ export const useModernI18n = <
295
171
  updateLanguage,
296
172
  localePathRedirect,
297
173
  ignoreRedirectRoutes,
298
- localisedUrls,
174
+ urlStrategy,
299
175
  languages,
300
176
  hasRouter,
301
177
  navigate,
@@ -306,7 +182,7 @@ export const useModernI18n = <
306
182
  updateLanguage,
307
183
  localePathRedirect,
308
184
  ignoreRedirectRoutes,
309
- localisedUrls,
185
+ urlStrategy,
310
186
  languages,
311
187
  hasRouter,
312
188
  navigate,
@@ -341,7 +217,7 @@ export const useModernI18n = <
341
217
  // narrows to the concrete instance type via the TInstance type argument.
342
218
  i18nInstance: i18nInstance as TInstance,
343
219
  supportedLanguages: languages || [],
344
- localisedUrls,
220
+ urlStrategy,
345
221
  isLanguageSupported,
346
222
  isResourcesReady,
347
223
  };
@@ -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;
@@ -0,0 +1,16 @@
1
+ import type { ComponentType } from 'react';
2
+ import { createContext } from 'react';
3
+ import type { ModernI18nContextValue } from './context';
4
+ import type { I18nRouterAdapter } from './routerAdapter';
5
+
6
+ // Every other i18n runtime module imports these through
7
+ // `@modern-js/plugin-i18n/runtime/contexts` so Module Federation shares one
8
+ // copy of them. Their values stay provider-owned.
9
+ export const ModernI18nContext = createContext<ModernI18nContextValue | null>(
10
+ null,
11
+ );
12
+ export const ReactI18nextProviderContext =
13
+ createContext<ComponentType<any> | null>(null);
14
+ export const I18nNavigationContext = createContext<I18nRouterAdapter | null>(
15
+ null,
16
+ );
@@ -1,15 +1,24 @@
1
1
  import type { RuntimePlugin } from '@modern-js/runtime';
2
2
  import type React from 'react';
3
+ import { resolveMappedUrlStrategy } from '../shared/mappedUrlStrategy';
3
4
  import type {
4
5
  BaseBackendOptions,
5
6
  BaseLocaleDetectionOptions,
6
7
  } from '../shared/type';
8
+ import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
7
9
  import type { I18nInitOptions, I18nInstance } from './i18n';
10
+ import { I18nRouterNavigationProvider } from './navigation';
8
11
  import {
9
12
  type RuntimeContextWithI18n,
10
13
  setupI18nBeforeRender,
11
14
  } from './pluginSetup';
12
- import { createI18nRootWrapper } from './providerComposition';
15
+ import {
16
+ createI18nRootWrapper,
17
+ type I18nLanguageSynchronizationProps,
18
+ } from './providerComposition';
19
+
20
+ export type { I18nLanguageSynchronizationProps } from './providerComposition';
21
+
13
22
  import {
14
23
  type LoadReactI18nextIntegration,
15
24
  resolveReactI18nextIntegration,
@@ -17,16 +26,28 @@ import {
17
26
  import './types';
18
27
 
19
28
  export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
29
+ export type { I18nUrlStrategy } from '../shared/urlStrategy';
20
30
  export type {
21
31
  I18nInitOptions,
22
32
  I18nInstance,
23
33
  Resources,
24
34
  TranslateFn,
25
35
  } from './i18n/instance';
36
+ export {
37
+ I18nRouterNavigationProvider,
38
+ useIntegratedRouterAdapter,
39
+ } from './navigation';
26
40
  export type {
27
41
  LoadReactI18nextIntegration,
28
42
  ReactI18nextIntegration,
29
43
  } from './reactI18next';
44
+ export {
45
+ I18nNavigationProvider,
46
+ type I18nRouterAdapter,
47
+ type I18nRouterLinkTarget,
48
+ useI18nRouterAdapter,
49
+ useNativeI18nRouterAdapter,
50
+ } from './routerAdapter';
30
51
 
31
52
  export interface I18nPluginOptions {
32
53
  entryName?: string;
@@ -37,6 +58,9 @@ export interface I18nPluginOptions {
37
58
  initOptions?: I18nInitOptions;
38
59
  htmlLangAttr?: boolean;
39
60
  reactI18next?: boolean;
61
+ urlStrategy?: I18nUrlStrategy;
62
+ NavigationProvider?: React.ComponentType<React.PropsWithChildren>;
63
+ LanguageSynchronization?: React.ComponentType<I18nLanguageSynchronizationProps>;
40
64
  [key: string]: any;
41
65
  }
42
66
 
@@ -55,6 +79,9 @@ export const createI18nPlugin =
55
79
  backend,
56
80
  htmlLangAttr = false,
57
81
  reactI18next = true,
82
+ urlStrategy,
83
+ NavigationProvider,
84
+ LanguageSynchronization,
58
85
  } = options;
59
86
  const {
60
87
  localePathRedirect = false,
@@ -63,9 +90,23 @@ export const createI18nPlugin =
63
90
  fallbackLanguage = 'en',
64
91
  detection,
65
92
  ignoreRedirectRoutes,
66
- localisedUrls,
67
93
  } = localeDetection || {};
68
94
  const { enabled: backendEnabled = false } = backend || {};
95
+ // A strategy configured in `modern.config.ts` reaches here through JSON,
96
+ // which drops its methods. Anything unusable is treated as absent so the
97
+ // built-in language-prefix behaviour applies instead of throwing.
98
+ //
99
+ // Falling back to `localeDetection.localisedUrls` makes mapped locale
100
+ // URLs work for a bare `appTools()` consumer: the map is plain data, so
101
+ // it survives the JSON boundary that a `urlStrategy` object does not.
102
+ // The server already derives its policy from the same field, and the two
103
+ // must agree — otherwise SSR renders `/cs/obchodni-podminky` and the
104
+ // hydrated `<Link>` navigates to `/cs/terms-of-service`, which 404s.
105
+ // An explicit, usable `urlStrategy` (supplied by a composing runtime
106
+ // plugin) still wins.
107
+ const resolvedUrlStrategy =
108
+ asI18nUrlStrategy(urlStrategy) ??
109
+ resolveMappedUrlStrategy(localeDetection);
69
110
  let latestI18nInstance: I18nInstance | undefined;
70
111
  let I18nextProvider: React.ComponentType<any> | null;
71
112
 
@@ -100,7 +141,15 @@ export const createI18nPlugin =
100
141
  languages,
101
142
  fallbackLanguage,
102
143
  ignoreRedirectRoutes,
103
- localisedUrls,
144
+ urlStrategy: resolvedUrlStrategy,
145
+ // The selected router, not react-router, is what a localized `<Link>`
146
+ // has to navigate through, and the i18n language has to follow a
147
+ // client-side navigation to a mapped URL. Both need the actual router
148
+ // instance, so a bare `appTools()` consumer gets the integrated
149
+ // adapter by default; a composing runtime plugin can still replace it.
150
+ NavigationProvider:
151
+ NavigationProvider ?? I18nRouterNavigationProvider,
152
+ LanguageSynchronization,
104
153
  getLatestI18nInstance: () => latestI18nInstance,
105
154
  getI18nextProvider: () => I18nextProvider,
106
155
  }),
@@ -128,12 +177,10 @@ export {
128
177
  type LinkProps,
129
178
  } from './Link';
130
179
  export {
131
- canonicalPath,
132
180
  type LocalizedPathsConfig,
133
- localizePath,
134
181
  type UseLocalizedLocationReturn,
135
182
  type UseLocalizedPathsReturn,
136
183
  useLocalizedLocation,
137
184
  useLocalizedPaths,
138
185
  } from './localizedPaths';
139
- export { buildLocalizedUrl, splitUrlTarget } from './utils';
186
+ export { splitUrlTarget } from './utils';
@@ -1,7 +1,7 @@
1
- import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
1
  import { isBrowser } from '@modern-js/runtime';
3
2
  import type React from 'react';
4
3
  import { useCallback, useEffect, useRef } from 'react';
4
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
5
5
  import {
6
6
  cacheI18nLanguage,
7
7
  changeI18nInstanceLanguage,
@@ -54,9 +54,8 @@ export function createContextValue(
54
54
  languages: string[],
55
55
  localePathRedirect: boolean,
56
56
  ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
57
- localisedUrls: LocalisedUrlsOption | undefined,
58
57
  setLang: (lang: string) => void,
59
- synchronizeLanguage: (lang: string) => void,
58
+ urlStrategy?: I18nUrlStrategy,
60
59
  ) {
61
60
  const instance = i18nInstance || createMinimalI18nInstance(lang);
62
61
  return {
@@ -66,9 +65,8 @@ export function createContextValue(
66
65
  languages,
67
66
  localePathRedirect,
68
67
  ignoreRedirectRoutes,
69
- localisedUrls,
68
+ urlStrategy,
70
69
  updateLanguage: setLang,
71
- synchronizeLanguage,
72
70
  };
73
71
  }
74
72
 
@@ -179,7 +177,7 @@ export function useClientSideRedirect(
179
177
  languages: string[],
180
178
  fallbackLanguage: string,
181
179
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
182
- localisedUrls?: LocalisedUrlsOption,
180
+ urlStrategy?: I18nUrlStrategy,
183
181
  ) {
184
182
  const urlRef = useRef('');
185
183
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
@@ -220,7 +218,14 @@ export function useClientSideRedirect(
220
218
  const entryPath = getEntryPath();
221
219
  const relativePath = currentPathname.replace(entryPath, '');
222
220
 
223
- if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) {
221
+ if (
222
+ shouldIgnoreRedirect(
223
+ relativePath,
224
+ languages,
225
+ ignoreRedirectRoutes,
226
+ urlStrategy,
227
+ )
228
+ ) {
224
229
  return;
225
230
  }
226
231
 
@@ -241,7 +246,7 @@ export function useClientSideRedirect(
241
246
  relativePath,
242
247
  targetLanguage,
243
248
  languages,
244
- localisedUrls,
249
+ urlStrategy,
245
250
  );
246
251
  const newUrl = entryPath + newPath + currentSearch + currentHash;
247
252
 
@@ -265,7 +270,7 @@ export function useClientSideRedirect(
265
270
  languages,
266
271
  fallbackLanguage,
267
272
  ignoreRedirectRoutes,
268
- localisedUrls,
273
+ urlStrategy,
269
274
  ]);
270
275
  }
271
276
 
@@ -74,8 +74,7 @@ export type TranslateFn = (
74
74
  // declares a top-level `[key: string]: any` and overloaded call-signature
75
75
  // properties; both make i18next's `i18n` structurally unassignable to this
76
76
  // type, so the documented `i18nInstance: i18next` usage does not typecheck.
77
- // Do NOT restore upstream's shape when resolving a sync merge — the guard is
78
- // tests/type-fixture/i18nInstanceTypes.fixture.ts.
77
+ // Do NOT restore upstream's shape when resolving a sync merge.
79
78
  export interface I18nInstance {
80
79
  language: string;
81
80
  isInitialized?: boolean;