@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.9 → 3.5.0-ultramodern.90

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 (221) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/cli/locales.js +15 -15
  3. package/dist/cjs/runtime/Link.js +8 -89
  4. package/dist/cjs/runtime/context.js +78 -91
  5. package/dist/cjs/runtime/contextHelpers.js +143 -0
  6. package/dist/cjs/runtime/core.js +32 -132
  7. package/dist/cjs/runtime/hooks.js +54 -21
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  9. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  10. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  11. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  12. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  13. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  14. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  15. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  16. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  17. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  18. package/dist/cjs/runtime/i18n/instance.js +34 -10
  19. package/dist/cjs/runtime/linkHelpers.js +148 -0
  20. package/dist/cjs/runtime/pluginSetup.js +102 -0
  21. package/dist/cjs/runtime/providerComposition.js +115 -0
  22. package/dist/cjs/runtime/reactI18next.js +43 -0
  23. package/dist/cjs/runtime/routerAdapter.js +16 -8
  24. package/dist/cjs/runtime/utils.js +1 -9
  25. package/dist/cjs/server/apiPrefix.js +72 -0
  26. package/dist/cjs/server/detectorOptions.js +97 -0
  27. package/dist/cjs/server/index.js +17 -144
  28. package/dist/cjs/server/redirectPolicy.js +96 -0
  29. package/dist/cjs/shared/detection.js +2 -104
  30. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  31. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  32. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  33. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  34. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  35. package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
  36. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  37. package/dist/cjs/shared/localisedUrls/routes.js +130 -0
  38. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  39. package/dist/cjs/shared/localisedUrls.js +28 -294
  40. package/dist/cjs/shared/utils.js +2 -8
  41. package/dist/esm/cli/index.mjs +2 -1
  42. package/dist/esm/cli/locales.mjs +13 -13
  43. package/dist/esm/runtime/Link.mjs +1 -82
  44. package/dist/esm/runtime/context.mjs +76 -92
  45. package/dist/esm/runtime/contextHelpers.mjs +87 -0
  46. package/dist/esm/runtime/core.mjs +30 -133
  47. package/dist/esm/runtime/hooks.mjs +56 -23
  48. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  49. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  50. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  51. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  52. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  54. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  55. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  56. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  57. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  58. package/dist/esm/runtime/i18n/instance.mjs +35 -8
  59. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  60. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  61. package/dist/esm/runtime/providerComposition.mjs +77 -0
  62. package/dist/esm/runtime/reactI18next.mjs +5 -0
  63. package/dist/esm/runtime/routerAdapter.mjs +16 -8
  64. package/dist/esm/runtime/utils.mjs +2 -10
  65. package/dist/esm/server/apiPrefix.mjs +31 -0
  66. package/dist/esm/server/detectorOptions.mjs +59 -0
  67. package/dist/esm/server/index.mjs +8 -135
  68. package/dist/esm/server/redirectPolicy.mjs +46 -0
  69. package/dist/esm/shared/detection.mjs +1 -71
  70. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  71. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  72. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  73. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  74. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  75. package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
  76. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  77. package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
  78. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  79. package/dist/esm/shared/localisedUrls.mjs +1 -283
  80. package/dist/esm/shared/utils.mjs +1 -1
  81. package/dist/esm-node/cli/index.mjs +2 -1
  82. package/dist/esm-node/cli/locales.mjs +13 -13
  83. package/dist/esm-node/runtime/Link.mjs +1 -82
  84. package/dist/esm-node/runtime/context.mjs +76 -92
  85. package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
  86. package/dist/esm-node/runtime/core.mjs +30 -133
  87. package/dist/esm-node/runtime/hooks.mjs +56 -23
  88. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  89. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  90. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  91. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  92. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  93. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  94. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  95. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  96. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  97. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  98. package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
  99. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  100. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  101. package/dist/esm-node/runtime/providerComposition.mjs +78 -0
  102. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  103. package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
  104. package/dist/esm-node/runtime/utils.mjs +2 -10
  105. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  106. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  107. package/dist/esm-node/server/index.mjs +8 -135
  108. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  109. package/dist/esm-node/shared/detection.mjs +1 -71
  110. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  111. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  112. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  113. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  114. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  115. package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
  116. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  117. package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
  118. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  119. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  120. package/dist/esm-node/shared/utils.mjs +1 -1
  121. package/dist/types/cli/locales.d.ts +1 -1
  122. package/dist/types/runtime/Link.d.ts +3 -7
  123. package/dist/types/runtime/context.d.ts +21 -5
  124. package/dist/types/runtime/contextHelpers.d.ts +29 -0
  125. package/dist/types/runtime/core.d.ts +4 -8
  126. package/dist/types/runtime/hooks.d.ts +3 -7
  127. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  128. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  129. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  130. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  131. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  132. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  133. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  134. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  135. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  136. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  137. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  138. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  139. package/dist/types/runtime/i18n/instance.d.ts +31 -27
  140. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  141. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  142. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  143. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  144. package/dist/types/runtime/providerComposition.d.ts +16 -0
  145. package/dist/types/runtime/reactI18next.d.ts +7 -0
  146. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  147. package/dist/types/runtime/utils.d.ts +7 -2
  148. package/dist/types/server/apiPrefix.d.ts +7 -0
  149. package/dist/types/server/detectorOptions.d.ts +21 -0
  150. package/dist/types/server/index.d.ts +1 -6
  151. package/dist/types/server/redirectPolicy.d.ts +22 -0
  152. package/dist/types/shared/detection.d.ts +1 -11
  153. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  154. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  155. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  156. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  157. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  158. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  159. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  160. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  161. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  162. package/dist/types/shared/localisedUrls.d.ts +2 -36
  163. package/dist/types/shared/utils.d.ts +0 -2
  164. package/package.json +17 -20
  165. package/rstest.config.mts +8 -1
  166. package/src/cli/index.ts +5 -4
  167. package/src/cli/locales.ts +3 -3
  168. package/src/runtime/Link.tsx +11 -138
  169. package/src/runtime/context.tsx +170 -207
  170. package/src/runtime/contextHelpers.ts +242 -0
  171. package/src/runtime/core.tsx +48 -275
  172. package/src/runtime/hooks.ts +86 -24
  173. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  174. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  175. package/src/runtime/i18n/detection/cache.ts +189 -0
  176. package/src/runtime/i18n/detection/detector.ts +166 -0
  177. package/src/runtime/i18n/detection/index.ts +10 -641
  178. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  179. package/src/runtime/i18n/detection/language.ts +64 -0
  180. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  181. package/src/runtime/i18n/detection/middleware.ts +15 -7
  182. package/src/runtime/i18n/detection/path.ts +41 -0
  183. package/src/runtime/i18n/detection/priority.ts +82 -0
  184. package/src/runtime/i18n/detection/ssr.ts +47 -0
  185. package/src/runtime/i18n/detection/types.ts +41 -0
  186. package/src/runtime/i18n/instance.ts +98 -34
  187. package/src/runtime/i18n/react-i18next.ts +1 -6
  188. package/src/runtime/i18n/utils.ts +18 -9
  189. package/src/runtime/linkHelpers.ts +174 -0
  190. package/src/runtime/pluginSetup.ts +189 -0
  191. package/src/runtime/providerComposition.tsx +148 -0
  192. package/src/runtime/reactI18next.ts +20 -0
  193. package/src/runtime/routerAdapter.tsx +36 -15
  194. package/src/runtime/utils.ts +13 -35
  195. package/src/server/apiPrefix.ts +67 -0
  196. package/src/server/detectorOptions.ts +105 -0
  197. package/src/server/index.ts +14 -319
  198. package/src/server/redirectPolicy.ts +127 -0
  199. package/src/shared/detection.ts +1 -1
  200. package/src/shared/localisedUrls/config.ts +18 -0
  201. package/src/shared/localisedUrls/index.ts +24 -0
  202. package/src/shared/localisedUrls/normalise.ts +67 -0
  203. package/src/shared/localisedUrls/pathname.ts +60 -0
  204. package/src/shared/localisedUrls/patterns.ts +160 -0
  205. package/src/shared/localisedUrls/redirect.ts +93 -0
  206. package/src/shared/localisedUrls/resolve.ts +135 -0
  207. package/src/shared/localisedUrls/routes.ts +234 -0
  208. package/src/shared/localisedUrls/types.ts +18 -0
  209. package/src/shared/localisedUrls.ts +26 -623
  210. package/src/shared/utils.ts +2 -2
  211. package/tests/federatedI18nBoundary.client.test.tsx +112 -0
  212. package/tests/federatedI18nBoundary.test.tsx +120 -0
  213. package/tests/fixtures/localised-routes.golden.json +156 -0
  214. package/tests/link.test.tsx +111 -0
  215. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  216. package/tests/localisedUrls.test.ts +148 -2
  217. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  218. package/tests/redirectPolicy.test.ts +45 -0
  219. package/tests/routerAdapter.test.tsx +603 -0
  220. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  221. package/tests/type-fixture/tsconfig.json +1 -0
@@ -11,7 +11,16 @@ import {
11
11
  isI18nWrapperInstance,
12
12
  } from './instance';
13
13
 
14
- export function assertI18nInstance(obj: any): asserts obj is I18nInstance {
14
+ type MergedBackendOptions = NonNullable<I18nInitOptions['backend']> & {
15
+ _useChainedBackend?: boolean;
16
+ _chainedBackendConfig?: {
17
+ backendOptions: Array<Record<string, unknown>>;
18
+ };
19
+ backends?: unknown[];
20
+ backendOptions?: unknown;
21
+ };
22
+
23
+ export function assertI18nInstance(obj: unknown): asserts obj is I18nInstance {
15
24
  if (!isI18nInstance(obj)) {
16
25
  throw new Error('Object does not implement I18nInstance interface');
17
26
  }
@@ -24,8 +33,8 @@ export const buildInitOptions = async (
24
33
  finalLanguage: string,
25
34
  fallbackLanguage: string,
26
35
  languages: string[],
27
- mergedDetection: any,
28
- mergedBackend: any,
36
+ mergedDetection: I18nInitOptions['detection'],
37
+ mergedBackend: MergedBackendOptions | undefined,
29
38
  userInitOptions?: I18nInitOptions,
30
39
  useSuspense?: boolean,
31
40
  i18nInstance?: I18nInstance,
@@ -84,8 +93,8 @@ export const buildInitOptions = async (
84
93
  if (isChainedBackend && mergedBackend._chainedBackendConfig) {
85
94
  // Try to get backend classes from i18nInstance.options.backend.backends first
86
95
  // This avoids importing fs-backend in browser environment
87
- let HttpBackend: any;
88
- let SdkBackendClass: any;
96
+ let HttpBackend: unknown;
97
+ let SdkBackendClass: unknown;
89
98
 
90
99
  if (
91
100
  i18nInstance?.options?.backend?.backends &&
@@ -148,7 +157,7 @@ export const changeI18nLanguage = async (
148
157
  i18nInstance: I18nInstance,
149
158
  newLang: string,
150
159
  options?: {
151
- detectionOptions?: any;
160
+ detectionOptions?: I18nInitOptions['detection'];
152
161
  },
153
162
  ): Promise<void> => {
154
163
  if (!newLang || typeof newLang !== 'string') {
@@ -179,8 +188,8 @@ export const initializeI18nInstance = async (
179
188
  finalLanguage: string,
180
189
  fallbackLanguage: string,
181
190
  languages: string[],
182
- mergedDetection: any,
183
- mergedBackend: any,
191
+ mergedDetection: I18nInitOptions['detection'],
192
+ mergedBackend: MergedBackendOptions | undefined,
184
193
  userInitOptions?: I18nInitOptions,
185
194
  useSuspense?: boolean,
186
195
  ): Promise<void> => {
@@ -265,7 +274,7 @@ export const setupClonedInstance = async (
265
274
  backendEnabled: boolean,
266
275
  backend: BaseBackendOptions | undefined,
267
276
  i18nextDetector: boolean,
268
- detection: any,
277
+ detection: I18nInitOptions['detection'],
269
278
  localePathRedirect: boolean,
270
279
  userInitOptions: I18nInitOptions | undefined,
271
280
  ): Promise<void> => {
@@ -0,0 +1,174 @@
1
+ const warnedTargets = new Set<string>();
2
+
3
+ export const warnOnce = (key: string, message: string) => {
4
+ if (process.env.NODE_ENV !== 'development' || warnedTargets.has(key)) {
5
+ return;
6
+ }
7
+ warnedTargets.add(key);
8
+ console.warn(message);
9
+ };
10
+
11
+ export type LinkParams = Record<string, string | number | undefined>;
12
+ type NormalizedSearchValue = string | string[];
13
+
14
+ /**
15
+ * Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
16
+ * (`$` / `*`) segments with concrete values before localization, so
17
+ * pattern-mapped slugs localize correctly.
18
+ */
19
+ export const interpolateRouteParams = (
20
+ pathname: string,
21
+ params?: LinkParams,
22
+ ): string => {
23
+ if (!/[$:*{]/.test(pathname)) {
24
+ return pathname;
25
+ }
26
+
27
+ const resolveParam = (name: string): string | undefined => {
28
+ const value = params?.[name];
29
+ return value === undefined ? undefined : String(value);
30
+ };
31
+
32
+ const segments = pathname
33
+ .split('/')
34
+ .map(segment => {
35
+ if (!segment) {
36
+ return segment;
37
+ }
38
+
39
+ if (segment.startsWith('{-$') && segment.endsWith('}')) {
40
+ const value = resolveParam(segment.slice(3, -1));
41
+ return value === undefined ? null : encodeURIComponent(value);
42
+ }
43
+
44
+ if (segment === '$' || segment === '*') {
45
+ const value = resolveParam('_splat') ?? resolveParam('*');
46
+ return value === undefined
47
+ ? null
48
+ : value.split('/').map(encodeURIComponent).join('/');
49
+ }
50
+
51
+ if (segment.startsWith('$')) {
52
+ const value = resolveParam(segment.slice(1));
53
+ if (value === undefined) {
54
+ warnOnce(
55
+ `missing-param:${pathname}:${segment}`,
56
+ `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`,
57
+ );
58
+ return segment;
59
+ }
60
+ return encodeURIComponent(value);
61
+ }
62
+
63
+ if (segment.startsWith(':')) {
64
+ const optional = segment.endsWith('?');
65
+ const name = segment.slice(1, optional ? -1 : undefined);
66
+ const value = resolveParam(name);
67
+ if (value === undefined) {
68
+ if (optional) {
69
+ return null;
70
+ }
71
+ warnOnce(
72
+ `missing-param:${pathname}:${segment}`,
73
+ `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`,
74
+ );
75
+ return segment;
76
+ }
77
+ return encodeURIComponent(value);
78
+ }
79
+
80
+ return segment;
81
+ })
82
+ .filter(segment => segment !== null);
83
+
84
+ return segments.join('/') || '/';
85
+ };
86
+
87
+ export const normalizeSearch = (
88
+ search: string | Record<string, unknown> | undefined,
89
+ searchFromTo: string,
90
+ ): {
91
+ searchString: string;
92
+ searchObject: Record<string, NormalizedSearchValue> | undefined;
93
+ } => {
94
+ if (search && typeof search === 'object') {
95
+ const searchObject: Record<string, NormalizedSearchValue> = {};
96
+ const params = new URLSearchParams();
97
+
98
+ for (const [key, value] of Object.entries(search)) {
99
+ if (value === undefined || value === null) {
100
+ continue;
101
+ }
102
+
103
+ if (Array.isArray(value)) {
104
+ const values = value
105
+ .filter(item => item !== undefined && item !== null)
106
+ .map(String);
107
+ if (values.length === 0) {
108
+ continue;
109
+ }
110
+ searchObject[key] = values;
111
+ for (const item of values) {
112
+ params.append(key, item);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ const stringValue = String(value);
118
+ searchObject[key] = stringValue;
119
+ params.append(key, stringValue);
120
+ }
121
+
122
+ const serialized = params.toString();
123
+ return {
124
+ searchString: serialized ? `?${serialized}` : '',
125
+ searchObject:
126
+ Object.keys(searchObject).length > 0 ? searchObject : undefined,
127
+ };
128
+ }
129
+
130
+ const raw = typeof search === 'string' ? search : searchFromTo;
131
+ if (!raw) {
132
+ return { searchString: '', searchObject: undefined };
133
+ }
134
+
135
+ const searchString = raw.startsWith('?') ? raw : `?${raw}`;
136
+ const searchObject: Record<string, NormalizedSearchValue> = {};
137
+ new URLSearchParams(searchString).forEach((value, key) => {
138
+ const existing = searchObject[key];
139
+ if (existing === undefined) {
140
+ searchObject[key] = value;
141
+ } else if (Array.isArray(existing)) {
142
+ existing.push(value);
143
+ } else {
144
+ searchObject[key] = [existing, value];
145
+ }
146
+ });
147
+
148
+ return { searchString, searchObject };
149
+ };
150
+
151
+ type ActivePropsResult<TActiveProps extends Record<string, unknown>> = Record<
152
+ string,
153
+ unknown
154
+ > &
155
+ Partial<TActiveProps>;
156
+
157
+ export const splitActiveProps = <TActiveProps extends Record<string, unknown>>(
158
+ active: boolean,
159
+ activeProps?: TActiveProps,
160
+ ): ActivePropsResult<TActiveProps> => {
161
+ if (!active || !activeProps) {
162
+ return {};
163
+ }
164
+ return activeProps;
165
+ };
166
+
167
+ export const mergeClassNames = (
168
+ ...values: Array<unknown>
169
+ ): string | undefined => {
170
+ const classNames = values.filter(
171
+ (value): value is string => typeof value === 'string' && value.length > 0,
172
+ );
173
+ return classNames.length > 0 ? classNames.join(' ') : undefined;
174
+ };
@@ -0,0 +1,189 @@
1
+ import { isBrowser } from '@modern-js/runtime';
2
+ import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
3
+ import { merge } from '@modern-js/runtime-utils/merge';
4
+ import type {
5
+ BaseBackendOptions,
6
+ BaseLocaleDetectionOptions,
7
+ } from '../shared/type';
8
+ import type { I18nInitOptions, I18nInstance } from './i18n';
9
+ import { getI18nInstance } from './i18n';
10
+ import { mergeBackendOptions } from './i18n/backend';
11
+ import {
12
+ detectLanguageWithPriority,
13
+ exportServerLngToWindow,
14
+ mergeDetectionOptions,
15
+ } from './i18n/detection';
16
+ import { useI18nextLanguageDetector } from './i18n/detection/middleware';
17
+ import type { ReactI18nextIntegration } from './reactI18next';
18
+ import { getPathname } from './utils';
19
+
20
+ type I18nLifecycleHelpers = {
21
+ useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
22
+ changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
23
+ ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
24
+ initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
25
+ setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
26
+ };
27
+
28
+ let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
29
+
30
+ function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
31
+ i18nLifecycleHelpersPromise ??= Promise.all([
32
+ import('./i18n/backend/middleware'),
33
+ import('./i18n/utils'),
34
+ ]).then(([backendMiddleware, utils]) => ({
35
+ useI18nextBackend: backendMiddleware.useI18nextBackend,
36
+ changeI18nLanguage: utils.changeI18nLanguage,
37
+ ensureLanguageMatch: utils.ensureLanguageMatch,
38
+ initializeI18nInstance: utils.initializeI18nInstance,
39
+ setupClonedInstance: utils.setupClonedInstance,
40
+ }));
41
+
42
+ return i18nLifecycleHelpersPromise;
43
+ }
44
+
45
+ interface RuntimeConfigWithI18n {
46
+ i18n?: {
47
+ initOptions?: I18nInitOptions;
48
+ };
49
+ }
50
+
51
+ interface I18nRuntimeApi {
52
+ getRuntimeConfig: () => RuntimeConfigWithI18n;
53
+ }
54
+
55
+ export interface RuntimeContextWithI18n extends TInternalRuntimeContext {
56
+ i18nInstance?: I18nInstance;
57
+ changeLanguage?: (lang: string) => Promise<void>;
58
+ }
59
+
60
+ interface SetupI18nBeforeRenderOptions {
61
+ api: I18nRuntimeApi;
62
+ userI18nInstance?: I18nInstance;
63
+ initOptions?: I18nInitOptions;
64
+ backend?: BaseBackendOptions;
65
+ backendEnabled: boolean;
66
+ i18nextDetector: boolean;
67
+ detection?: BaseLocaleDetectionOptions['detection'];
68
+ localePathRedirect: boolean;
69
+ languages: string[];
70
+ fallbackLanguage: string;
71
+ resolveReactI18nextIntegration: () => Promise<ReactI18nextIntegration | null>;
72
+ setI18nextProvider: (
73
+ provider: ReactI18nextIntegration['I18nextProvider'],
74
+ ) => void;
75
+ }
76
+
77
+ export async function setupI18nBeforeRender(
78
+ context: RuntimeContextWithI18n,
79
+ options: SetupI18nBeforeRenderOptions,
80
+ ): Promise<I18nInstance> {
81
+ const {
82
+ api,
83
+ userI18nInstance,
84
+ initOptions,
85
+ backend,
86
+ backendEnabled,
87
+ i18nextDetector,
88
+ detection,
89
+ localePathRedirect,
90
+ languages,
91
+ fallbackLanguage,
92
+ resolveReactI18nextIntegration,
93
+ setI18nextProvider,
94
+ } = options;
95
+ const {
96
+ useI18nextBackend,
97
+ changeI18nLanguage,
98
+ ensureLanguageMatch,
99
+ initializeI18nInstance,
100
+ setupClonedInstance,
101
+ } = await loadI18nLifecycleHelpers();
102
+ let i18nInstance = await getI18nInstance(userI18nInstance);
103
+ const { i18n: otherConfig } = api.getRuntimeConfig();
104
+ const { initOptions: otherInitOptions } = otherConfig || {};
105
+ const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
106
+
107
+ const reactI18nextIntegration = await resolveReactI18nextIntegration();
108
+ setI18nextProvider(reactI18nextIntegration?.I18nextProvider ?? null);
109
+ if (reactI18nextIntegration?.initReactI18next) {
110
+ i18nInstance.use(reactI18nextIntegration.initReactI18next);
111
+ }
112
+
113
+ const pathname = getPathname(context);
114
+ if (i18nextDetector) {
115
+ useI18nextLanguageDetector(i18nInstance);
116
+ }
117
+
118
+ const mergedDetection = mergeDetectionOptions(
119
+ i18nextDetector,
120
+ detection,
121
+ localePathRedirect,
122
+ userInitOptions,
123
+ );
124
+ const mergedBackend = mergeBackendOptions(backend, userInitOptions);
125
+
126
+ // Register Backend BEFORE detectLanguageWithPriority because detection may init.
127
+ const hasSdkConfig =
128
+ typeof userInitOptions?.backend?.sdk === 'function' ||
129
+ (mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
130
+ if (mergedBackend && (backendEnabled || hasSdkConfig)) {
131
+ useI18nextBackend(i18nInstance, mergedBackend);
132
+ }
133
+
134
+ const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
135
+ languages,
136
+ fallbackLanguage,
137
+ localePathRedirect,
138
+ i18nextDetector,
139
+ detection,
140
+ userInitOptions,
141
+ mergedBackend,
142
+ pathname,
143
+ ssrContext: context.ssrContext,
144
+ });
145
+
146
+ await initializeI18nInstance(
147
+ i18nInstance,
148
+ finalLanguage,
149
+ fallbackLanguage,
150
+ languages,
151
+ mergedDetection,
152
+ mergedBackend,
153
+ userInitOptions,
154
+ );
155
+
156
+ if (!isBrowser() && i18nInstance.cloneInstance) {
157
+ i18nInstance = i18nInstance.cloneInstance();
158
+ await setupClonedInstance(
159
+ i18nInstance,
160
+ finalLanguage,
161
+ fallbackLanguage,
162
+ languages,
163
+ backendEnabled,
164
+ backend,
165
+ i18nextDetector,
166
+ detection,
167
+ localePathRedirect,
168
+ userInitOptions,
169
+ );
170
+ }
171
+
172
+ if (localePathRedirect) {
173
+ await ensureLanguageMatch(i18nInstance, finalLanguage);
174
+ }
175
+
176
+ if (!isBrowser()) {
177
+ exportServerLngToWindow(context, finalLanguage);
178
+ }
179
+ context.i18nInstance = i18nInstance;
180
+
181
+ // Add changeLanguage method to context for other runtime plugins to use.
182
+ context.changeLanguage = async (newLang: string) => {
183
+ await changeI18nLanguage(i18nInstance, newLang, {
184
+ detectionOptions: mergedDetection,
185
+ });
186
+ };
187
+
188
+ return i18nInstance;
189
+ }
@@ -0,0 +1,148 @@
1
+ import { RuntimeContext } from '@modern-js/runtime';
2
+ import { Helmet } from '@modern-js/runtime/head';
3
+ import type React from 'react';
4
+ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
5
+ import type { BaseLocaleDetectionOptions } from '../shared/type';
6
+ import { ModernI18nProvider } from './context';
7
+ import {
8
+ createContextValue,
9
+ useClientSideRedirect,
10
+ useLanguageSync,
11
+ useSdkResourcesLoader,
12
+ } from './hooks';
13
+ import type { I18nInstance } from './i18n';
14
+ import { getI18nextInstanceForProvider } from './i18n/instance';
15
+ import type { RuntimeContextWithI18n } from './pluginSetup';
16
+ import { useI18nRouterAdapter } from './routerAdapter';
17
+
18
+ interface I18nRootWrapperOptions {
19
+ entryName?: string;
20
+ htmlLangAttr: boolean;
21
+ localePathRedirect: boolean;
22
+ languages: string[];
23
+ fallbackLanguage: string;
24
+ ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
25
+ localisedUrls?: BaseLocaleDetectionOptions['localisedUrls'];
26
+ getLatestI18nInstance: () => I18nInstance | undefined;
27
+ getI18nextProvider: () => React.ComponentType<any> | null | undefined;
28
+ }
29
+
30
+ export const createI18nRootWrapper =
31
+ (options: I18nRootWrapperOptions) => (App: React.ComponentType<any>) => {
32
+ return (props: Record<string, unknown>) => {
33
+ const {
34
+ entryName,
35
+ htmlLangAttr,
36
+ localePathRedirect,
37
+ languages,
38
+ fallbackLanguage,
39
+ ignoreRedirectRoutes,
40
+ localisedUrls,
41
+ getLatestI18nInstance,
42
+ getI18nextProvider,
43
+ } = options;
44
+ const runtimeContext = useContext(
45
+ RuntimeContext,
46
+ ) as RuntimeContextWithI18n;
47
+ const i18nInstance =
48
+ runtimeContext.i18nInstance || getLatestI18nInstance();
49
+ const initialLang = useMemo(
50
+ () => i18nInstance?.language || fallbackLanguage,
51
+ [i18nInstance?.language, fallbackLanguage],
52
+ );
53
+ const [lang, setLang] = useState(initialLang);
54
+ const [forceUpdate, setForceUpdate] = useState(0);
55
+ const prevLangRef = useRef(lang);
56
+ const { location } = useI18nRouterAdapter();
57
+
58
+ useEffect(() => {
59
+ if (i18nInstance?.language) {
60
+ const translator = (i18nInstance as any).translator;
61
+ if (translator) {
62
+ translator.language = i18nInstance.language;
63
+ }
64
+ }
65
+ }, [i18nInstance?.language]);
66
+
67
+ useEffect(() => {
68
+ prevLangRef.current = lang;
69
+ }, [lang]);
70
+
71
+ useSdkResourcesLoader(i18nInstance, setForceUpdate);
72
+ const synchronizeLanguage = useLanguageSync(
73
+ i18nInstance,
74
+ localePathRedirect,
75
+ languages,
76
+ location?.pathname,
77
+ prevLangRef,
78
+ setLang,
79
+ );
80
+
81
+ // Handle client-side redirect for static deployments.
82
+ useClientSideRedirect(
83
+ i18nInstance,
84
+ localePathRedirect,
85
+ languages,
86
+ fallbackLanguage,
87
+ ignoreRedirectRoutes,
88
+ localisedUrls,
89
+ );
90
+
91
+ const contextValue = useMemo(
92
+ () =>
93
+ createContextValue(
94
+ lang,
95
+ i18nInstance,
96
+ entryName,
97
+ languages,
98
+ localePathRedirect,
99
+ ignoreRedirectRoutes,
100
+ localisedUrls,
101
+ setLang,
102
+ synchronizeLanguage,
103
+ ),
104
+ [
105
+ lang,
106
+ i18nInstance,
107
+ entryName,
108
+ languages,
109
+ localePathRedirect,
110
+ ignoreRedirectRoutes,
111
+ localisedUrls,
112
+ forceUpdate,
113
+ synchronizeLanguage,
114
+ ],
115
+ );
116
+
117
+ const children = (props as React.PropsWithChildren).children;
118
+ let appContent: React.ReactNode = App ? (
119
+ <App {...props}>{children}</App>
120
+ ) : (
121
+ children
122
+ );
123
+
124
+ if (i18nInstance) {
125
+ const I18nextProvider = getI18nextProvider();
126
+ if (I18nextProvider) {
127
+ const i18nextInstanceForProvider = getI18nextInstanceForProvider(
128
+ i18nInstance,
129
+ lang,
130
+ );
131
+ appContent = (
132
+ <I18nextProvider i18n={i18nextInstanceForProvider}>
133
+ {appContent}
134
+ </I18nextProvider>
135
+ );
136
+ }
137
+ }
138
+
139
+ return (
140
+ <>
141
+ {Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
142
+ <ModernI18nProvider value={contextValue}>
143
+ {appContent}
144
+ </ModernI18nProvider>
145
+ </>
146
+ );
147
+ };
148
+ };
@@ -0,0 +1,20 @@
1
+ import type React from 'react';
2
+
3
+ export interface ReactI18nextIntegration {
4
+ I18nextProvider: React.ComponentType<any> | null;
5
+ initReactI18next: any | null;
6
+ }
7
+
8
+ export type LoadReactI18nextIntegration =
9
+ () => Promise<ReactI18nextIntegration | null>;
10
+
11
+ export const resolveReactI18nextIntegration = async (
12
+ reactI18next: boolean,
13
+ loadReactI18nextIntegration?: LoadReactI18nextIntegration,
14
+ ): Promise<ReactI18nextIntegration | null> => {
15
+ if (!reactI18next) {
16
+ return null;
17
+ }
18
+
19
+ return loadReactI18nextIntegration?.() ?? null;
20
+ };