@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
@@ -4,12 +4,19 @@ import type {
4
4
  BaseBackendOptions,
5
5
  BaseLocaleDetectionOptions,
6
6
  } from '../shared/type';
7
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
7
8
  import type { I18nInitOptions, I18nInstance } from './i18n';
8
9
  import {
9
10
  type RuntimeContextWithI18n,
10
11
  setupI18nBeforeRender,
11
12
  } from './pluginSetup';
12
- import { createI18nRootWrapper } from './providerComposition';
13
+ import {
14
+ createI18nRootWrapper,
15
+ type I18nLanguageSynchronizationProps,
16
+ } from './providerComposition';
17
+
18
+ export type { I18nLanguageSynchronizationProps } from './providerComposition';
19
+
13
20
  import {
14
21
  type LoadReactI18nextIntegration,
15
22
  resolveReactI18nextIntegration,
@@ -17,6 +24,7 @@ import {
17
24
  import './types';
18
25
 
19
26
  export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
27
+ export type { I18nUrlStrategy } from '../shared/urlStrategy';
20
28
  export type {
21
29
  I18nInitOptions,
22
30
  I18nInstance,
@@ -27,6 +35,13 @@ export type {
27
35
  LoadReactI18nextIntegration,
28
36
  ReactI18nextIntegration,
29
37
  } from './reactI18next';
38
+ export {
39
+ I18nNavigationProvider,
40
+ type I18nRouterAdapter,
41
+ type I18nRouterLinkTarget,
42
+ useI18nRouterAdapter,
43
+ useNativeI18nRouterAdapter,
44
+ } from './routerAdapter';
30
45
 
31
46
  export interface I18nPluginOptions {
32
47
  entryName?: string;
@@ -37,6 +52,9 @@ export interface I18nPluginOptions {
37
52
  initOptions?: I18nInitOptions;
38
53
  htmlLangAttr?: boolean;
39
54
  reactI18next?: boolean;
55
+ urlStrategy?: I18nUrlStrategy;
56
+ NavigationProvider?: React.ComponentType<React.PropsWithChildren>;
57
+ LanguageSynchronization?: React.ComponentType<I18nLanguageSynchronizationProps>;
40
58
  [key: string]: any;
41
59
  }
42
60
 
@@ -55,6 +73,9 @@ export const createI18nPlugin =
55
73
  backend,
56
74
  htmlLangAttr = false,
57
75
  reactI18next = true,
76
+ urlStrategy,
77
+ NavigationProvider,
78
+ LanguageSynchronization,
58
79
  } = options;
59
80
  const {
60
81
  localePathRedirect = false,
@@ -63,7 +84,6 @@ export const createI18nPlugin =
63
84
  fallbackLanguage = 'en',
64
85
  detection,
65
86
  ignoreRedirectRoutes,
66
- localisedUrls,
67
87
  } = localeDetection || {};
68
88
  const { enabled: backendEnabled = false } = backend || {};
69
89
  let latestI18nInstance: I18nInstance | undefined;
@@ -100,7 +120,9 @@ export const createI18nPlugin =
100
120
  languages,
101
121
  fallbackLanguage,
102
122
  ignoreRedirectRoutes,
103
- localisedUrls,
123
+ urlStrategy,
124
+ NavigationProvider,
125
+ LanguageSynchronization,
104
126
  getLatestI18nInstance: () => latestI18nInstance,
105
127
  getI18nextProvider: () => I18nextProvider,
106
128
  }),
@@ -128,12 +150,10 @@ export {
128
150
  type LinkProps,
129
151
  } from './Link';
130
152
  export {
131
- canonicalPath,
132
153
  type LocalizedPathsConfig,
133
- localizePath,
134
154
  type UseLocalizedLocationReturn,
135
155
  type UseLocalizedPathsReturn,
136
156
  useLocalizedLocation,
137
157
  useLocalizedPaths,
138
158
  } from './localizedPaths';
139
- export { buildLocalizedUrl, splitUrlTarget } from './utils';
159
+ 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
 
@@ -1,20 +1,17 @@
1
- import {
2
- canonicalTargetPathname,
3
- type LocalisedUrlsOption,
4
- } from '@modern-js/i18n-runtime-extensions';
5
1
  import { useMemo } from 'react';
2
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
6
3
  import { useModernI18n } from './context';
7
4
  import { useI18nRouterAdapter } from './routerAdapter';
8
5
  import { buildLocalizedUrl, splitUrlTarget } from './utils';
9
6
 
10
7
  export interface LocalizedPathsConfig {
11
8
  languages: string[];
12
- localisedUrls?: LocalisedUrlsOption;
9
+ urlStrategy?: I18nUrlStrategy;
13
10
  }
14
11
 
15
12
  /**
16
13
  * Localize a canonical, language-agnostic target for the given language:
17
- * adds the language prefix and applies `localisedUrls` pattern mapping.
14
+ * adds the language prefix and applies the configured URL strategy.
18
15
  * `?search`/`#hash` suffixes are preserved verbatim.
19
16
  */
20
17
  export const localizePath = (
@@ -22,7 +19,7 @@ export const localizePath = (
22
19
  language: string,
23
20
  config: LocalizedPathsConfig,
24
21
  ): string =>
25
- buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
22
+ buildLocalizedUrl(pathname, language, config.languages, config.urlStrategy);
26
23
 
27
24
  /**
28
25
  * Reverse of {@link localizePath}: strip the language prefix and map localized
@@ -34,11 +31,19 @@ export const canonicalPath = (
34
31
  config: LocalizedPathsConfig,
35
32
  ): string => {
36
33
  const { pathname, search, hash } = splitUrlTarget(target);
37
- const resolvedPath = canonicalTargetPathname(
38
- pathname,
39
- config.languages,
40
- config.localisedUrls,
41
- );
34
+ if (config.urlStrategy) {
35
+ return `${config.urlStrategy.canonicalPathname(pathname, config.languages)}${search}${hash}`;
36
+ }
37
+ const segments = pathname.split('/').filter(Boolean);
38
+ if (
39
+ segments[0] &&
40
+ config.languages.some(
41
+ language => language.toLowerCase() === segments[0].toLowerCase(),
42
+ )
43
+ ) {
44
+ segments.shift();
45
+ }
46
+ const resolvedPath = `/${segments.join('/')}`;
42
47
 
43
48
  return `${resolvedPath}${search}${hash}`;
44
49
  };
@@ -50,16 +55,16 @@ export interface UseLocalizedPathsReturn {
50
55
 
51
56
  /**
52
57
  * Context-bound versions of {@link localizePath} and {@link canonicalPath} —
53
- * the plugin configuration (languages, localisedUrls) is read from the i18n
58
+ * the languages and URL strategy are read from the i18n
54
59
  * provider, so apps never copy pattern-matching helpers again.
55
60
  */
56
61
  export const useLocalizedPaths = (): UseLocalizedPathsReturn => {
57
- const { supportedLanguages, localisedUrls } = useModernI18n();
62
+ const { supportedLanguages, urlStrategy } = useModernI18n();
58
63
 
59
64
  return useMemo(() => {
60
65
  const config: LocalizedPathsConfig = {
61
66
  languages: supportedLanguages,
62
- localisedUrls,
67
+ urlStrategy,
63
68
  };
64
69
 
65
70
  return {
@@ -67,7 +72,7 @@ export const useLocalizedPaths = (): UseLocalizedPathsReturn => {
67
72
  localizePath(pathname, language, config),
68
73
  canonicalPath: (pathname: string) => canonicalPath(pathname, config),
69
74
  };
70
- }, [supportedLanguages, localisedUrls]);
75
+ }, [supportedLanguages, urlStrategy]);
71
76
  };
72
77
 
73
78
  export interface UseLocalizedLocationReturn {
@@ -83,7 +88,7 @@ export interface UseLocalizedLocationReturn {
83
88
  * language switchers. SSR-safe: the location comes from the router adapter.
84
89
  */
85
90
  export const useLocalizedLocation = (): UseLocalizedLocationReturn => {
86
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
91
+ const { language, supportedLanguages, urlStrategy } = useModernI18n();
87
92
  const { location } = useI18nRouterAdapter();
88
93
  const pathname = location?.pathname ?? '/';
89
94
  const search = location?.search ?? '';
@@ -92,7 +97,7 @@ export const useLocalizedLocation = (): UseLocalizedLocationReturn => {
92
97
  return useMemo(() => {
93
98
  const config: LocalizedPathsConfig = {
94
99
  languages: supportedLanguages,
95
- localisedUrls,
100
+ urlStrategy,
96
101
  };
97
102
  const alternates: Record<string, string> = {};
98
103
  for (const supportedLanguage of supportedLanguages) {
@@ -105,5 +110,5 @@ export const useLocalizedLocation = (): UseLocalizedLocationReturn => {
105
110
  canonical: canonicalPath(pathname, config),
106
111
  alternates,
107
112
  };
108
- }, [language, supportedLanguages, localisedUrls, pathname, search, hash]);
113
+ }, [language, supportedLanguages, urlStrategy, pathname, search, hash]);
109
114
  };
@@ -1,13 +1,14 @@
1
- import { useLanguageSync } from '@modern-js/i18n-runtime-extensions';
2
1
  import { RuntimeContext } from '@modern-js/runtime';
3
2
  import { Helmet } from '@modern-js/runtime/head';
4
3
  import type React from 'react';
5
4
  import { useContext, useEffect, useMemo, useRef, useState } from 'react';
6
5
  import type { BaseLocaleDetectionOptions } from '../shared/type';
6
+ import type { I18nUrlStrategy } from '../shared/urlStrategy';
7
7
  import { ModernI18nProvider } from './context';
8
8
  import {
9
9
  createContextValue,
10
10
  useClientSideRedirect,
11
+ useLanguageSync,
11
12
  useSdkResourcesLoader,
12
13
  } from './hooks';
13
14
  import type { I18nInstance } from './i18n';
@@ -15,6 +16,38 @@ import { getI18nextInstanceForProvider } from './i18n/instance';
15
16
  import type { RuntimeContextWithI18n } from './pluginSetup';
16
17
  import { useI18nRouterAdapter } from './routerAdapter';
17
18
 
19
+ export interface I18nLanguageSynchronizationProps {
20
+ i18nInstance: I18nInstance | undefined;
21
+ localePathRedirect: boolean;
22
+ languages: string[];
23
+ pathname: string | undefined;
24
+ prevLangRef: React.MutableRefObject<string>;
25
+ setLang: (language: string) => void;
26
+ children: (
27
+ synchronizeLanguage: (language: string) => void,
28
+ ) => React.ReactNode;
29
+ }
30
+
31
+ const NativeLanguageSynchronization = ({
32
+ i18nInstance,
33
+ localePathRedirect,
34
+ languages,
35
+ pathname,
36
+ prevLangRef,
37
+ setLang,
38
+ children,
39
+ }: I18nLanguageSynchronizationProps) => {
40
+ const synchronizeLanguage = useLanguageSync(
41
+ i18nInstance,
42
+ localePathRedirect,
43
+ languages,
44
+ pathname,
45
+ prevLangRef,
46
+ setLang,
47
+ );
48
+ return children(synchronizeLanguage);
49
+ };
50
+
18
51
  interface I18nRootWrapperOptions {
19
52
  entryName?: string;
20
53
  htmlLangAttr: boolean;
@@ -22,14 +55,18 @@ interface I18nRootWrapperOptions {
22
55
  languages: string[];
23
56
  fallbackLanguage: string;
24
57
  ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
25
- localisedUrls?: BaseLocaleDetectionOptions['localisedUrls'];
58
+ urlStrategy?: I18nUrlStrategy;
59
+ NavigationProvider?: React.ComponentType<React.PropsWithChildren>;
60
+ LanguageSynchronization?: React.ComponentType<I18nLanguageSynchronizationProps>;
26
61
  getLatestI18nInstance: () => I18nInstance | undefined;
27
62
  getI18nextProvider: () => React.ComponentType<any> | null | undefined;
28
63
  }
29
64
 
30
65
  export const createI18nRootWrapper =
31
66
  (options: I18nRootWrapperOptions) => (App: React.ComponentType<any>) => {
32
- return (props: Record<string, unknown>) => {
67
+ const LanguageSynchronization =
68
+ options.LanguageSynchronization ?? NativeLanguageSynchronization;
69
+ const I18nRoot = (props: Record<string, unknown>) => {
33
70
  const {
34
71
  entryName,
35
72
  htmlLangAttr,
@@ -37,7 +74,7 @@ export const createI18nRootWrapper =
37
74
  languages,
38
75
  fallbackLanguage,
39
76
  ignoreRedirectRoutes,
40
- localisedUrls,
77
+ urlStrategy,
41
78
  getLatestI18nInstance,
42
79
  getI18nextProvider,
43
80
  } = options;
@@ -69,15 +106,6 @@ export const createI18nRootWrapper =
69
106
  }, [lang]);
70
107
 
71
108
  useSdkResourcesLoader(i18nInstance, setForceUpdate);
72
- const synchronizeLanguage = useLanguageSync(
73
- i18nInstance,
74
- localePathRedirect,
75
- languages,
76
- location?.pathname,
77
- prevLangRef,
78
- setLang,
79
- );
80
-
81
109
  // Handle client-side redirect for static deployments.
82
110
  useClientSideRedirect(
83
111
  i18nInstance,
@@ -85,7 +113,7 @@ export const createI18nRootWrapper =
85
113
  languages,
86
114
  fallbackLanguage,
87
115
  ignoreRedirectRoutes,
88
- localisedUrls,
116
+ urlStrategy,
89
117
  );
90
118
 
91
119
  const contextValue = useMemo(
@@ -97,9 +125,8 @@ export const createI18nRootWrapper =
97
125
  languages,
98
126
  localePathRedirect,
99
127
  ignoreRedirectRoutes,
100
- localisedUrls,
101
128
  setLang,
102
- synchronizeLanguage,
129
+ urlStrategy,
103
130
  ),
104
131
  [
105
132
  lang,
@@ -108,9 +135,8 @@ export const createI18nRootWrapper =
108
135
  languages,
109
136
  localePathRedirect,
110
137
  ignoreRedirectRoutes,
111
- localisedUrls,
138
+ urlStrategy,
112
139
  forceUpdate,
113
- synchronizeLanguage,
114
140
  ],
115
141
  );
116
142
  const I18nextProvider = getI18nextProvider();
@@ -140,13 +166,35 @@ export const createI18nRootWrapper =
140
166
  return (
141
167
  <>
142
168
  {Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
143
- <ModernI18nProvider
144
- i18nextProvider={I18nextProvider}
145
- value={contextValue}
169
+ <LanguageSynchronization
170
+ i18nInstance={i18nInstance}
171
+ localePathRedirect={localePathRedirect}
172
+ languages={languages}
173
+ pathname={location?.pathname}
174
+ prevLangRef={prevLangRef}
175
+ setLang={setLang}
146
176
  >
147
- {appContent}
148
- </ModernI18nProvider>
177
+ {synchronizeLanguage => (
178
+ <ModernI18nProvider
179
+ i18nextProvider={I18nextProvider}
180
+ value={{ ...contextValue, synchronizeLanguage }}
181
+ >
182
+ {appContent}
183
+ </ModernI18nProvider>
184
+ )}
185
+ </LanguageSynchronization>
149
186
  </>
150
187
  );
151
188
  };
189
+ const { NavigationProvider } = options;
190
+ if (!NavigationProvider) {
191
+ return I18nRoot;
192
+ }
193
+ return function I18nRootWithNavigation(props: Record<string, unknown>) {
194
+ return (
195
+ <NavigationProvider>
196
+ <I18nRoot {...props} />
197
+ </NavigationProvider>
198
+ );
199
+ };
152
200
  };