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

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 (143) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +46 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +26 -61
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/core.js +27 -14
  7. package/dist/cjs/runtime/hooks.js +7 -8
  8. package/dist/cjs/runtime/localizedPaths.js +11 -9
  9. package/dist/cjs/runtime/navigation.js +48 -0
  10. package/dist/cjs/runtime/providerComposition.js +37 -13
  11. package/dist/cjs/runtime/routerAdapter.js +35 -140
  12. package/dist/cjs/runtime/utils.js +21 -19
  13. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  14. package/dist/cjs/server/index.js +11 -7
  15. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  16. package/dist/cjs/server/redirectPolicy.js +33 -24
  17. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  18. package/dist/cjs/shared/urlStrategy.js +48 -0
  19. package/dist/esm/cli/index.mjs +10 -21
  20. package/dist/esm/cli/localisedRoutes.mjs +8 -0
  21. package/dist/esm/runtime/Link.mjs +19 -32
  22. package/dist/esm/runtime/context.mjs +18 -62
  23. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  24. package/dist/esm/runtime/core.mjs +13 -6
  25. package/dist/esm/runtime/hooks.mjs +7 -8
  26. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  27. package/dist/esm/runtime/navigation.mjs +7 -0
  28. package/dist/esm/runtime/providerComposition.mjs +38 -14
  29. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  30. package/dist/esm/runtime/utils.mjs +19 -17
  31. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  32. package/dist/esm/server/index.mjs +11 -7
  33. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  34. package/dist/esm/server/redirectPolicy.mjs +33 -24
  35. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  36. package/dist/esm/shared/urlStrategy.mjs +7 -0
  37. package/dist/esm-node/cli/index.mjs +10 -21
  38. package/dist/esm-node/cli/localisedRoutes.mjs +9 -0
  39. package/dist/esm-node/runtime/Link.mjs +19 -32
  40. package/dist/esm-node/runtime/context.mjs +18 -62
  41. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  42. package/dist/esm-node/runtime/core.mjs +13 -6
  43. package/dist/esm-node/runtime/hooks.mjs +7 -8
  44. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  45. package/dist/esm-node/runtime/navigation.mjs +8 -0
  46. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  47. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  48. package/dist/esm-node/runtime/utils.mjs +19 -17
  49. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  50. package/dist/esm-node/server/index.mjs +11 -7
  51. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  52. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  53. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  54. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  55. package/dist/types/cli/index.d.ts +2 -2
  56. package/dist/types/cli/locales.d.ts +1 -1
  57. package/dist/types/cli/localisedRoutes.d.ts +18 -0
  58. package/dist/types/runtime/Link.d.ts +2 -2
  59. package/dist/types/runtime/context.d.ts +10 -13
  60. package/dist/types/runtime/contextHelpers.d.ts +3 -3
  61. package/dist/types/runtime/core.d.ts +23 -13
  62. package/dist/types/runtime/hooks.d.ts +5 -6
  63. package/dist/types/runtime/i18n/backend/config.d.ts +1 -1
  64. package/dist/types/runtime/i18n/backend/index.d.ts +2 -2
  65. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -2
  66. package/dist/types/runtime/i18n/backend/middleware.d.ts +2 -2
  67. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +2 -2
  68. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +1 -1
  69. package/dist/types/runtime/i18n/detection/cache.d.ts +1 -1
  70. package/dist/types/runtime/i18n/detection/config.d.ts +1 -1
  71. package/dist/types/runtime/i18n/detection/detector.d.ts +2 -2
  72. package/dist/types/runtime/i18n/detection/index.d.ts +4 -4
  73. package/dist/types/runtime/i18n/detection/initOptions.d.ts +1 -1
  74. package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
  75. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -1
  76. package/dist/types/runtime/i18n/detection/priority.d.ts +2 -2
  77. package/dist/types/runtime/i18n/detection/types.d.ts +1 -1
  78. package/dist/types/runtime/i18n/index.d.ts +3 -3
  79. package/dist/types/runtime/i18n/instance.d.ts +1 -1
  80. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -1
  81. package/dist/types/runtime/i18n/utils.d.ts +2 -2
  82. package/dist/types/runtime/index.d.ts +2 -2
  83. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  84. package/dist/types/runtime/navigation.d.ts +10 -0
  85. package/dist/types/runtime/no-react-i18next.d.ts +2 -2
  86. package/dist/types/runtime/pluginSetup.d.ts +3 -3
  87. package/dist/types/runtime/providerComposition.d.ts +15 -3
  88. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  89. package/dist/types/runtime/types.d.ts +1 -1
  90. package/dist/types/runtime/utils.d.ts +4 -12
  91. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  92. package/dist/types/server/detectorOptions.d.ts +1 -1
  93. package/dist/types/server/index.d.ts +3 -1
  94. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  95. package/dist/types/server/redirectPolicy.d.ts +5 -13
  96. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  97. package/dist/types/shared/type.d.ts +8 -12
  98. package/dist/types/shared/urlStrategy.d.ts +18 -0
  99. package/dist/types/shared/utils.d.ts +1 -1
  100. package/package.json +30 -18
  101. package/rstest.config.mts +6 -5
  102. package/src/cli/index.ts +42 -43
  103. package/src/cli/localisedRoutes.ts +43 -0
  104. package/src/runtime/Link.tsx +20 -40
  105. package/src/runtime/context.tsx +34 -133
  106. package/src/runtime/contextHelpers.ts +7 -5
  107. package/src/runtime/core.tsx +53 -6
  108. package/src/runtime/hooks.ts +14 -9
  109. package/src/runtime/i18n/instance.ts +1 -2
  110. package/src/runtime/localizedPaths.ts +24 -19
  111. package/src/runtime/navigation.tsx +20 -0
  112. package/src/runtime/providerComposition.tsx +71 -23
  113. package/src/runtime/routerAdapter.tsx +73 -338
  114. package/src/runtime/utils.ts +49 -31
  115. package/src/server/defaultUrlStrategy.ts +27 -0
  116. package/src/server/index.ts +39 -9
  117. package/src/server/mappedUrlStrategy.ts +10 -0
  118. package/src/server/redirectPolicy.ts +85 -70
  119. package/src/shared/mappedUrlStrategy.ts +59 -0
  120. package/src/shared/type.ts +7 -11
  121. package/src/shared/urlStrategy.ts +38 -0
  122. package/tests/backendDefaults.test.ts +2 -23
  123. package/tests/defaultUrlStrategy.test.ts +107 -0
  124. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  125. package/tests/i18nUtils.test.ts +31 -65
  126. package/tests/link.test.tsx +13 -511
  127. package/tests/localisedRouteGeneration.test.ts +159 -0
  128. package/tests/localisedUrls.test.ts +16 -208
  129. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  130. package/tests/navigation.test.tsx +365 -0
  131. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  132. package/tests/redirectPolicy.test.ts +89 -27
  133. package/tests/routerAdapter.test.tsx +98 -1171
  134. package/tests/urlStrategySerialization.test.ts +122 -0
  135. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  136. package/tests/federatedI18nBoundary.test.tsx +0 -132
  137. package/tests/linkTypes.test.ts +0 -59
  138. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  139. package/tests/peerCohort.test.ts +0 -25
  140. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  141. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  142. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  143. 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,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;
@@ -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;