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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +47 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +30 -73
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/contexts.js +49 -0
  7. package/dist/cjs/runtime/core.js +27 -14
  8. package/dist/cjs/runtime/hooks.js +7 -8
  9. package/dist/cjs/runtime/localizedPaths.js +11 -9
  10. package/dist/cjs/runtime/navigation.js +48 -0
  11. package/dist/cjs/runtime/providerComposition.js +37 -13
  12. package/dist/cjs/runtime/routerAdapter.js +33 -140
  13. package/dist/cjs/runtime/sharedContexts.js +43 -0
  14. package/dist/cjs/runtime/utils.js +21 -19
  15. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  16. package/dist/cjs/server/index.js +11 -7
  17. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  18. package/dist/cjs/server/redirectPolicy.js +33 -24
  19. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  20. package/dist/cjs/shared/urlStrategy.js +48 -0
  21. package/dist/esm/cli/index.mjs +10 -21
  22. package/dist/esm/cli/localisedRoutes.mjs +9 -0
  23. package/dist/esm/runtime/Link.mjs +19 -32
  24. package/dist/esm/runtime/context.mjs +20 -72
  25. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  26. package/dist/esm/runtime/contexts.mjs +5 -0
  27. package/dist/esm/runtime/core.mjs +13 -6
  28. package/dist/esm/runtime/hooks.mjs +7 -8
  29. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  30. package/dist/esm/runtime/navigation.mjs +7 -0
  31. package/dist/esm/runtime/providerComposition.mjs +38 -14
  32. package/dist/esm/runtime/routerAdapter.mjs +28 -141
  33. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  34. package/dist/esm/runtime/utils.mjs +19 -17
  35. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  36. package/dist/esm/server/index.mjs +11 -7
  37. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  38. package/dist/esm/server/redirectPolicy.mjs +33 -24
  39. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  40. package/dist/esm/shared/urlStrategy.mjs +7 -0
  41. package/dist/esm-node/cli/index.mjs +10 -21
  42. package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
  43. package/dist/esm-node/runtime/Link.mjs +19 -32
  44. package/dist/esm-node/runtime/context.mjs +20 -72
  45. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  46. package/dist/esm-node/runtime/contexts.mjs +6 -0
  47. package/dist/esm-node/runtime/core.mjs +13 -6
  48. package/dist/esm-node/runtime/hooks.mjs +7 -8
  49. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  50. package/dist/esm-node/runtime/navigation.mjs +8 -0
  51. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  52. package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
  53. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  54. package/dist/esm-node/runtime/utils.mjs +19 -17
  55. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  56. package/dist/esm-node/server/index.mjs +11 -7
  57. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  58. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  59. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  60. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  61. package/dist/types/cli/localisedRoutes.d.ts +19 -0
  62. package/dist/types/runtime/context.d.ts +8 -11
  63. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  64. package/dist/types/runtime/contexts.d.ts +6 -0
  65. package/dist/types/runtime/core.d.ts +12 -2
  66. package/dist/types/runtime/hooks.d.ts +4 -5
  67. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  68. package/dist/types/runtime/navigation.d.ts +10 -0
  69. package/dist/types/runtime/providerComposition.d.ts +13 -1
  70. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  71. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  72. package/dist/types/runtime/utils.d.ts +4 -12
  73. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  74. package/dist/types/server/index.d.ts +2 -0
  75. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  76. package/dist/types/server/redirectPolicy.d.ts +5 -13
  77. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  78. package/dist/types/shared/type.d.ts +7 -11
  79. package/dist/types/shared/urlStrategy.d.ts +18 -0
  80. package/package.json +42 -19
  81. package/rslib.config.mts +8 -1
  82. package/rstest.config.mts +6 -5
  83. package/src/cli/index.ts +42 -42
  84. package/src/cli/localisedRoutes.ts +53 -0
  85. package/src/runtime/Link.tsx +20 -40
  86. package/src/runtime/context.tsx +33 -157
  87. package/src/runtime/contextHelpers.ts +7 -5
  88. package/src/runtime/contexts.ts +16 -0
  89. package/src/runtime/core.tsx +53 -6
  90. package/src/runtime/hooks.ts +14 -9
  91. package/src/runtime/i18n/instance.ts +1 -2
  92. package/src/runtime/localizedPaths.ts +24 -19
  93. package/src/runtime/navigation.tsx +20 -0
  94. package/src/runtime/providerComposition.tsx +71 -23
  95. package/src/runtime/routerAdapter.tsx +65 -339
  96. package/src/runtime/sharedContexts.ts +8 -0
  97. package/src/runtime/utils.ts +49 -31
  98. package/src/server/defaultUrlStrategy.ts +27 -0
  99. package/src/server/index.ts +39 -9
  100. package/src/server/mappedUrlStrategy.ts +10 -0
  101. package/src/server/redirectPolicy.ts +85 -70
  102. package/src/shared/mappedUrlStrategy.ts +59 -0
  103. package/src/shared/type.ts +7 -11
  104. package/src/shared/urlStrategy.ts +38 -0
  105. package/tests/backendDefaults.test.ts +2 -23
  106. package/tests/defaultUrlStrategy.test.ts +107 -0
  107. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  108. package/tests/i18nUtils.test.ts +31 -65
  109. package/tests/link.test.tsx +13 -511
  110. package/tests/localisedRouteGeneration.test.ts +200 -0
  111. package/tests/localisedUrls.test.ts +16 -208
  112. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  113. package/tests/navigation.test.tsx +403 -0
  114. package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
  115. package/tests/redirectPolicy.test.ts +89 -27
  116. package/tests/routerAdapter.test.tsx +98 -1171
  117. package/tests/urlStrategySerialization.test.ts +122 -0
  118. package/tsconfig.json +4 -2
  119. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  120. package/tests/federatedI18nBoundary.test.tsx +0 -132
  121. package/tests/linkTypes.test.ts +0 -59
  122. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  123. package/tests/peerCohort.test.ts +0 -25
  124. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  125. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  126. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  127. package/tests/type-fixture/tsconfig.json +0 -16
@@ -1,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
  };
@@ -0,0 +1,20 @@
1
+ import { createI18nRouterNavigation } from '@modern-js/i18n-runtime-extensions/router-navigation';
2
+ import {
3
+ I18nNavigationProvider,
4
+ type I18nRouterAdapter,
5
+ useNativeI18nRouterAdapter,
6
+ } from './routerAdapter';
7
+
8
+ /**
9
+ * The seam, and only the seam. The selected-router navigation adapter is a
10
+ * fork subsystem, so it lives in the fork-owned
11
+ * `@modern-js/i18n-runtime-extensions` and is attached here through the
12
+ * `NavigationProvider` extension point this package already exposes. The
13
+ * factory takes this package's own primitives as arguments, so the fork
14
+ * package never imports back into it and the dependency stays one-way.
15
+ */
16
+ export const { I18nRouterNavigationProvider, useIntegratedRouterAdapter } =
17
+ createI18nRouterNavigation<I18nRouterAdapter>({
18
+ I18nNavigationProvider,
19
+ useNativeI18nRouterAdapter,
20
+ });
@@ -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
  };