@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.5 → 3.5.0-ultramodern.75

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 (222) 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/linkTypes.test.ts +15 -4
  216. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  217. package/tests/localisedUrls.test.ts +148 -2
  218. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  219. package/tests/redirectPolicy.test.ts +45 -0
  220. package/tests/routerAdapter.test.tsx +603 -0
  221. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  222. package/tests/type-fixture/tsconfig.json +1 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Normalize language code (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en')
3
+ */
4
+ const normalizeLanguageCode = (language: string): string => {
5
+ if (!language) {
6
+ return language;
7
+ }
8
+ // Extract base language code (before hyphen)
9
+ const baseLang = language.split('-')[0];
10
+ return baseLang;
11
+ };
12
+
13
+ /**
14
+ * Check if a language is supported
15
+ * Also checks the base language code (e.g., 'zh-CN' matches 'zh')
16
+ */
17
+ export const isLanguageSupported = (
18
+ language: string | undefined,
19
+ supportedLanguages: string[],
20
+ ): boolean => {
21
+ if (!language) {
22
+ return false;
23
+ }
24
+ if (supportedLanguages.length === 0) {
25
+ return true;
26
+ }
27
+ // Check exact match first
28
+ if (supportedLanguages.includes(language)) {
29
+ return true;
30
+ }
31
+ // Check base language code match (e.g., 'zh-CN' matches 'zh')
32
+ const baseLang = normalizeLanguageCode(language);
33
+ if (baseLang !== language && supportedLanguages.includes(baseLang)) {
34
+ return true;
35
+ }
36
+ return false;
37
+ };
38
+
39
+ /**
40
+ * Get the supported language that matches the given language
41
+ * Returns the exact match if available, otherwise returns the base language code match
42
+ * Returns undefined if no match is found
43
+ */
44
+ export const getSupportedLanguage = (
45
+ language: string | undefined,
46
+ supportedLanguages: string[],
47
+ ): string | undefined => {
48
+ if (!language) {
49
+ return undefined;
50
+ }
51
+ if (supportedLanguages.length === 0) {
52
+ return language;
53
+ }
54
+ // Check exact match first
55
+ if (supportedLanguages.includes(language)) {
56
+ return language;
57
+ }
58
+ // Check base language code match (e.g., 'zh-CN' matches 'zh')
59
+ const baseLang = normalizeLanguageCode(language);
60
+ if (baseLang !== language && supportedLanguages.includes(baseLang)) {
61
+ return baseLang;
62
+ }
63
+ return undefined;
64
+ };
@@ -1,10 +1,22 @@
1
1
  import { LanguageDetector } from 'i18next-http-middleware';
2
- import type { I18nInstance } from '../instance';
2
+ import type { I18nInstance, LanguageDetectorOptions } from '../instance';
3
+
4
+ type HttpDetectorInit = (
5
+ services: NonNullable<I18nInstance['services']>,
6
+ options?: unknown,
7
+ allOptions?: unknown,
8
+ ) => void;
9
+
10
+ type HttpDetectorDetect = (
11
+ request: unknown,
12
+ response: unknown,
13
+ detectionOrder?: unknown,
14
+ ) => string | string[] | undefined;
3
15
 
4
16
  export const cacheUserLanguage = (
5
17
  _i18nInstance: I18nInstance,
6
18
  _language: string,
7
- _detectionOptions?: any,
19
+ _detectionOptions?: unknown,
8
20
  ): void => {
9
21
  return;
10
22
  };
@@ -14,7 +26,7 @@ export const cacheUserLanguage = (
14
26
  * Not available in Node.js environment, returns undefined
15
27
  */
16
28
  export const readLanguageFromStorage = (
17
- _detectionOptions?: any,
29
+ _detectionOptions?: LanguageDetectorOptions,
18
30
  ): string | undefined => {
19
31
  // In Node.js environment, storage-based detection is not available
20
32
  return undefined;
@@ -36,8 +48,8 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
36
48
  */
37
49
  export const detectLanguage = (
38
50
  i18nInstance: I18nInstance,
39
- request?: any,
40
- detectionOptions?: any,
51
+ request?: unknown,
52
+ detectionOptions?: LanguageDetectorOptions,
41
53
  ): string | undefined => {
42
54
  if (!request) {
43
55
  return undefined;
@@ -65,9 +77,16 @@ export const detectLanguage = (
65
77
  const optionsToUse = detectionOptions
66
78
  ? { ...i18nInstance.options, detection: detectionOptions }
67
79
  : i18nInstance.options;
68
- manualDetector.init(i18nInstance.services, optionsToUse as any);
80
+ (manualDetector.init as unknown as HttpDetectorInit)(
81
+ i18nInstance.services,
82
+ optionsToUse,
83
+ );
69
84
 
70
- const result = (manualDetector.detect as any)(request, {}, undefined);
85
+ const result = (manualDetector.detect as unknown as HttpDetectorDetect)(
86
+ request,
87
+ {},
88
+ undefined,
89
+ );
71
90
  if (typeof result === 'string') {
72
91
  return result;
73
92
  }
@@ -1,8 +1,10 @@
1
1
  import { isBrowser } from '@modern-js/runtime';
2
2
  import LanguageDetector from 'i18next-browser-languagedetector';
3
- import type { I18nInstance } from '../instance';
3
+ import type { I18nInstance, LanguageDetectorOptions } from '../instance';
4
4
  import { getActualI18nextInstance, isI18nWrapperInstance } from '../instance';
5
5
 
6
+ type BrowserDetectorInitOptions = Parameters<LanguageDetector['init']>[1];
7
+
6
8
  /**
7
9
  * Register LanguageDetector plugin to i18n instance
8
10
  * Must be called before init() to properly register the detector
@@ -27,7 +29,7 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
27
29
  * Fallback when detector is not available in services
28
30
  */
29
31
  export const readLanguageFromStorage = (
30
- detectionOptions?: any,
32
+ detectionOptions?: LanguageDetectorOptions,
31
33
  ): string | undefined => {
32
34
  try {
33
35
  const options = detectionOptions || {};
@@ -112,8 +114,8 @@ export const readLanguageFromStorage = (
112
114
  */
113
115
  export const detectLanguage = (
114
116
  i18nInstance: I18nInstance,
115
- _request?: any,
116
- detectionOptions?: any,
117
+ _request?: unknown,
118
+ detectionOptions?: LanguageDetectorOptions,
117
119
  ): string | undefined => {
118
120
  try {
119
121
  // For wrapper instances, get the underlying i18next instance
@@ -160,7 +162,10 @@ export const detectLanguage = (
160
162
  const mergedOptions = detectionOptions
161
163
  ? { ...optionsToUse, detection: detectionOptions }
162
164
  : optionsToUse;
163
- manualDetector.init(servicesToUse, mergedOptions as any);
165
+ manualDetector.init(
166
+ servicesToUse,
167
+ mergedOptions as unknown as BrowserDetectorInitOptions,
168
+ );
164
169
 
165
170
  const result = manualDetector.detect();
166
171
  if (typeof result === 'string') {
@@ -186,7 +191,7 @@ export const detectLanguage = (
186
191
  export const cacheUserLanguage = (
187
192
  i18nInstance: I18nInstance,
188
193
  language: string,
189
- detectionOptions?: any,
194
+ detectionOptions?: unknown,
190
195
  ): void => {
191
196
  if (typeof window === 'undefined') {
192
197
  return;
@@ -231,7 +236,10 @@ export const cacheUserLanguage = (
231
236
  : optionsToUse;
232
237
 
233
238
  const manualDetector = new LanguageDetector();
234
- manualDetector.init(servicesToUse, mergedOptions as any);
239
+ manualDetector.init(
240
+ servicesToUse,
241
+ mergedOptions as unknown as BrowserDetectorInitOptions,
242
+ );
235
243
 
236
244
  if (typeof manualDetector.cacheUserLanguage === 'function') {
237
245
  manualDetector.cacheUserLanguage(language);
@@ -0,0 +1,41 @@
1
+ import { detectLanguageFromPath } from '../../utils';
2
+
3
+ /**
4
+ * Priority 2: Detect language from URL path
5
+ * Only returns a language if the path explicitly contains a language prefix
6
+ */
7
+ export const detectLanguageFromPathPriority = (
8
+ pathname: string,
9
+ languages: string[],
10
+ localePathRedirect: boolean,
11
+ ): string | undefined => {
12
+ if (!localePathRedirect) {
13
+ return undefined;
14
+ }
15
+
16
+ // If no languages are configured, cannot detect from path
17
+ if (!languages || languages.length === 0) {
18
+ return undefined;
19
+ }
20
+
21
+ // If pathname is empty or invalid, no language in path
22
+ if (!pathname || pathname.trim() === '') {
23
+ return undefined;
24
+ }
25
+
26
+ try {
27
+ const pathDetection = detectLanguageFromPath(
28
+ pathname,
29
+ languages,
30
+ localePathRedirect,
31
+ );
32
+ // Only return language if explicitly detected in path
33
+ if (pathDetection.detected === true && pathDetection.language) {
34
+ return pathDetection.language;
35
+ }
36
+ } catch (error) {
37
+ // Silently ignore errors, return undefined
38
+ }
39
+
40
+ return undefined;
41
+ };
@@ -0,0 +1,82 @@
1
+ import { type I18nInstance, isI18nWrapperInstance } from '../instance';
2
+
3
+ import { detectLanguageFromI18nextDetector } from './detector';
4
+
5
+ import { mergeDetectionOptions } from './initOptions';
6
+ import { readLanguageFromStorage } from './middleware';
7
+ import { detectLanguageFromPathPriority } from './path';
8
+ import { detectLanguageFromSSR } from './ssr';
9
+
10
+ import type {
11
+ LanguageDetectionOptions,
12
+ LanguageDetectionResult,
13
+ } from './types';
14
+
15
+ /**
16
+ * Detect language with priority:
17
+ * Priority 1: Path detection
18
+ * Priority 2: SSR data (try window._SSR_DATA first, works for both SSR and CSR)
19
+ * Priority 3: i18next detector (reads from cookie/localStorage)
20
+ * Priority 4: User config language or fallback
21
+ */
22
+ export const detectLanguageWithPriority = async (
23
+ i18nInstance: I18nInstance,
24
+ options: LanguageDetectionOptions,
25
+ ): Promise<LanguageDetectionResult> => {
26
+ const {
27
+ languages,
28
+ fallbackLanguage,
29
+ localePathRedirect,
30
+ i18nextDetector,
31
+ detection,
32
+ userInitOptions,
33
+ pathname,
34
+ ssrContext,
35
+ } = options;
36
+
37
+ let detectedLanguage: string | undefined;
38
+
39
+ // Priority 1: Path detection. Explicit URL locale must win over stale
40
+ // hydration data after client-side navigation.
41
+ detectedLanguage = detectLanguageFromPathPriority(
42
+ pathname,
43
+ languages,
44
+ localePathRedirect,
45
+ );
46
+
47
+ // Priority 2: Try SSR data when the current path has no explicit locale.
48
+ if (!detectedLanguage) {
49
+ detectedLanguage = detectLanguageFromSSR(languages);
50
+ }
51
+
52
+ // Priority 3: i18next detector (reads from cookie/localStorage)
53
+ if (!detectedLanguage && i18nextDetector) {
54
+ if (isI18nWrapperInstance(i18nInstance)) {
55
+ detectedLanguage = readLanguageFromStorage(
56
+ mergeDetectionOptions(
57
+ i18nextDetector,
58
+ detection,
59
+ localePathRedirect,
60
+ userInitOptions,
61
+ ),
62
+ );
63
+ } else {
64
+ detectedLanguage = await detectLanguageFromI18nextDetector(i18nInstance, {
65
+ languages,
66
+ fallbackLanguage,
67
+ localePathRedirect,
68
+ i18nextDetector,
69
+ detection,
70
+ userInitOptions,
71
+ mergedBackend: options.mergedBackend,
72
+ ssrContext,
73
+ });
74
+ }
75
+ }
76
+
77
+ // Priority 4: Use user config language or fallback
78
+ const finalLanguage =
79
+ detectedLanguage || userInitOptions?.lng || fallbackLanguage;
80
+
81
+ return { detectedLanguage, finalLanguage };
82
+ };
@@ -0,0 +1,47 @@
1
+ import { isBrowser, type TRuntimeContext } from '@modern-js/runtime';
2
+ import { getSupportedLanguage } from './language';
3
+
4
+ export function exportServerLngToWindow(context: TRuntimeContext, lng: string) {
5
+ context.__i18nData__ = { lng };
6
+ }
7
+
8
+ export const getLanguageFromSSRData = (window: Window): string | undefined => {
9
+ try {
10
+ const ssrData = (window as any)._SSR_DATA;
11
+ // Check if SSR data exists and has valid structure
12
+ if (!ssrData || !ssrData.data || !ssrData.data.i18nData) {
13
+ return undefined;
14
+ }
15
+ const lng = ssrData.data.i18nData.lng;
16
+ // Return language only if it's a non-empty string
17
+ return typeof lng === 'string' && lng.trim() !== '' ? lng : undefined;
18
+ } catch (error) {
19
+ // If accessing window._SSR_DATA throws an error, return undefined
20
+ return undefined;
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Priority 1: Detect language from SSR data
26
+ * Try to get language from window._SSR_DATA first (both SSR and CSR projects)
27
+ * Returns undefined if SSR data is not available or invalid
28
+ */
29
+ export const detectLanguageFromSSR = (
30
+ languages: string[],
31
+ ): string | undefined => {
32
+ if (!isBrowser()) {
33
+ return undefined;
34
+ }
35
+
36
+ try {
37
+ const ssrLanguage = getLanguageFromSSRData(window);
38
+ const supportedLanguage = getSupportedLanguage(ssrLanguage, languages);
39
+ if (supportedLanguage) {
40
+ return supportedLanguage;
41
+ }
42
+ } catch (error) {
43
+ // Silently ignore errors
44
+ }
45
+
46
+ return undefined;
47
+ };
@@ -0,0 +1,41 @@
1
+ import type {
2
+ BackendOptions,
3
+ I18nInitOptions,
4
+ LanguageDetectorOptions,
5
+ } from '../instance';
6
+
7
+ export interface LanguageDetectionSsrContext {
8
+ request?: unknown;
9
+ }
10
+
11
+ export interface BaseLanguageDetectionOptions {
12
+ languages: string[];
13
+ fallbackLanguage: string;
14
+ localePathRedirect: boolean;
15
+ i18nextDetector: boolean;
16
+ detection?: LanguageDetectorOptions;
17
+ userInitOptions?: I18nInitOptions;
18
+ mergedBackend?: BackendOptions;
19
+ }
20
+
21
+ export interface LanguageDetectionOptions extends BaseLanguageDetectionOptions {
22
+ pathname: string;
23
+ ssrContext?: LanguageDetectionSsrContext;
24
+ }
25
+
26
+ export interface LanguageDetectionResult {
27
+ detectedLanguage?: string;
28
+ finalLanguage: string;
29
+ }
30
+
31
+ /**
32
+ * Options for building i18n init options
33
+ */
34
+ export interface BuildInitOptionsParams {
35
+ finalLanguage: string;
36
+ fallbackLanguage: string;
37
+ languages: string[];
38
+ userInitOptions?: I18nInitOptions;
39
+ mergedDetection?: LanguageDetectorOptions;
40
+ mergeBackend?: BackendOptions;
41
+ }
@@ -1,9 +1,9 @@
1
1
  import type { BaseBackendOptions } from '../../shared/type';
2
2
 
3
- export interface I18nResourceStore {
3
+ interface I18nResourceStore {
4
4
  data?: {
5
5
  [language: string]: {
6
- [namespace: string]: string | { [key: string]: any };
6
+ [namespace: string]: ResourceValue;
7
7
  };
8
8
  };
9
9
  addResourceBundle?: (
@@ -15,30 +15,49 @@ export interface I18nResourceStore {
15
15
  ) => void;
16
16
  }
17
17
 
18
- export function isI18nWrapperInstance(obj: any): boolean {
18
+ type I18nWrapperInstance = I18nInstance & {
19
+ i18nInstance: {
20
+ instance: I18nInstance;
21
+ };
22
+ };
23
+
24
+ export function isI18nWrapperInstance(
25
+ obj: unknown,
26
+ ): obj is I18nWrapperInstance {
19
27
  if (!obj || typeof obj !== 'object') {
20
28
  return false;
21
29
  }
22
- if (!obj.i18nInstance || typeof obj.i18nInstance !== 'object') {
30
+ const candidate = obj as {
31
+ i18nInstance?: unknown;
32
+ init?: unknown;
33
+ use?: unknown;
34
+ };
35
+ if (!candidate.i18nInstance || typeof candidate.i18nInstance !== 'object') {
23
36
  return false;
24
37
  }
25
- if (!obj.i18nInstance.instance) {
38
+ const wrapper = candidate.i18nInstance as { instance?: unknown };
39
+ if (!wrapper.instance) {
26
40
  return false;
27
41
  }
28
- if (typeof obj.init !== 'function' || typeof obj.use !== 'function') {
42
+ if (
43
+ typeof candidate.init !== 'function' ||
44
+ typeof candidate.use !== 'function'
45
+ ) {
29
46
  return false;
30
47
  }
31
48
  return true;
32
49
  }
33
50
 
34
- export function getI18nWrapperI18nextInstance(wrapperInstance: any): any {
51
+ function getI18nWrapperI18nextInstance(
52
+ wrapperInstance: unknown,
53
+ ): I18nInstance | null {
35
54
  if (isI18nWrapperInstance(wrapperInstance)) {
36
55
  return wrapperInstance.i18nInstance?.instance;
37
56
  }
38
57
  return null;
39
58
  }
40
59
 
41
- export function getActualI18nextInstance(instance: I18nInstance | any): any {
60
+ export function getActualI18nextInstance(instance: I18nInstance): I18nInstance {
42
61
  if (isI18nWrapperInstance(instance)) {
43
62
  const i18nextInstance = getI18nWrapperI18nextInstance(instance);
44
63
  return i18nextInstance || instance;
@@ -50,39 +69,43 @@ export interface I18nInstance {
50
69
  language: string;
51
70
  isInitialized?: boolean;
52
71
  init: {
53
- (callback?: (error: any, t: any) => void): Promise<any>;
72
+ (callback?: (error: unknown, t: unknown) => void): Promise<unknown>;
54
73
  (
55
74
  options: I18nInitOptions,
56
- callback?: (error: any, t: any) => void,
57
- ): Promise<any>;
75
+ callback?: (error: unknown, t: unknown) => void,
76
+ ): Promise<unknown>;
58
77
  };
59
78
  changeLanguage?: (
60
79
  lng?: string,
61
- callback?: (error: any, t: any) => void,
62
- ) => Promise<any>;
80
+ callback?: (error: unknown, t: unknown) => void,
81
+ ) => Promise<unknown>;
63
82
  setLang?: (lang: string) => void | Promise<void>;
64
- use: (plugin: any) => void;
83
+ use: (plugin: unknown) => void;
65
84
  createInstance?: (options?: I18nInitOptions) => I18nInstance;
66
- cloneInstance?: () => I18nInstance; // ssr need
85
+ cloneInstance?: (options?: I18nInitOptions) => I18nInstance; // ssr need
67
86
  // i18next store (may not be in type definition but exists at runtime)
68
87
  store?: I18nResourceStore;
69
- emit?: (event: string, ...args: any[]) => void;
88
+ emit?: (event: string, ...args: unknown[]) => void;
70
89
  reloadResources?: (language?: string, namespace?: string) => Promise<void>;
90
+ removeResourceBundle?: (language: string, namespace: string) => I18nInstance;
71
91
  services?: {
72
92
  languageDetector?: {
73
- detect: (request?: any, options?: any) => string | string[] | undefined;
74
- [key: string]: any;
93
+ detect: (
94
+ request?: unknown,
95
+ options?: unknown,
96
+ ) => string | string[] | undefined;
97
+ [key: string]: unknown;
75
98
  };
76
99
  resourceStore?: I18nResourceStore;
77
- backend?: any; // Backend instance (e.g., SdkBackend)
78
- [key: string]: any;
100
+ backend?: unknown; // Backend instance (e.g., SdkBackend)
101
+ [key: string]: unknown;
79
102
  };
80
103
  // i18next instance options (available after initialization)
81
104
  options?: {
82
105
  backend?: BackendOptions;
83
- [key: string]: any;
106
+ [key: string]: unknown;
84
107
  };
85
- [key: string]: any;
108
+ [key: string]: unknown;
86
109
  }
87
110
 
88
111
  type LanguageDetectorOrder = string[];
@@ -101,12 +124,12 @@ export interface LanguageDetectorOptions {
101
124
  }
102
125
 
103
126
  export interface BackendOptions extends Omit<BaseBackendOptions, 'enabled'> {
104
- parse?: (data: string) => any;
105
- stringify?: (data: any) => string;
127
+ parse?: (data: string) => unknown;
128
+ stringify?: (data: unknown) => string;
106
129
  [key: string]: any;
107
130
  }
108
131
 
109
- export type ResourceValue = string | { [key: string]: ResourceValue };
132
+ type ResourceValue = string | { [key: string]: ResourceValue };
110
133
 
111
134
  export interface Resources {
112
135
  [lng: string]: {
@@ -126,15 +149,16 @@ export type I18nInitOptions = {
126
149
  defaultNS?: string | string[];
127
150
  interpolation?: {
128
151
  escapeValue?: boolean;
129
- [key: string]: any;
152
+ [key: string]: unknown;
130
153
  };
131
154
  react?: {
132
155
  useSuspense?: boolean;
133
- [key: string]: any;
156
+ [key: string]: unknown;
134
157
  };
158
+ forkResourceStore?: boolean;
135
159
  };
136
160
 
137
- export function isI18nInstance(obj: any): obj is I18nInstance {
161
+ export function isI18nInstance(obj: unknown): obj is I18nInstance {
138
162
  if (!obj || typeof obj !== 'object') {
139
163
  return false;
140
164
  }
@@ -143,7 +167,10 @@ export function isI18nInstance(obj: any): obj is I18nInstance {
143
167
  return true;
144
168
  }
145
169
 
146
- return typeof obj.init === 'function' && typeof obj.use === 'function';
170
+ const candidate = obj as { init?: unknown; use?: unknown };
171
+ return (
172
+ typeof candidate.init === 'function' && typeof candidate.use === 'function'
173
+ );
147
174
  }
148
175
 
149
176
  async function tryImportI18next(): Promise<I18nInstance | null> {
@@ -170,20 +197,57 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
170
197
  }
171
198
 
172
199
  export function getI18nextInstanceForProvider(
173
- instance: I18nInstance | any,
174
- ): any {
200
+ instance: I18nInstance,
201
+ language?: string,
202
+ ): I18nInstance {
203
+ let providerInstance = instance;
175
204
  if (isI18nWrapperInstance(instance)) {
176
205
  const i18nextInstance = getI18nWrapperI18nextInstance(instance);
177
206
  if (i18nextInstance) {
178
- return i18nextInstance;
207
+ providerInstance = i18nextInstance;
179
208
  }
180
209
  }
181
210
 
182
- return instance;
211
+ if (!language || typeof Proxy === 'undefined') {
212
+ return providerInstance;
213
+ }
214
+
215
+ return new Proxy(providerInstance, {
216
+ get(target, property, receiver) {
217
+ if (property === 'language' || property === 'resolvedLanguage') {
218
+ return language;
219
+ }
220
+ if (property === 'languages') {
221
+ const languages = Reflect.get(target, property, receiver);
222
+ return Array.isArray(languages)
223
+ ? [language, ...languages.filter(item => item !== language)]
224
+ : [language];
225
+ }
226
+ if (property === 't' && typeof target.getFixedT === 'function') {
227
+ return target.getFixedT(language);
228
+ }
229
+ if (
230
+ property === 'hasLoadedNamespace' &&
231
+ typeof target.hasLoadedNamespace === 'function'
232
+ ) {
233
+ const hasLoadedNamespace = target.hasLoadedNamespace as (
234
+ this: I18nInstance,
235
+ namespace: string,
236
+ options?: Record<string, unknown>,
237
+ ) => boolean;
238
+ return (namespace: string, options?: Record<string, unknown>) =>
239
+ hasLoadedNamespace.call(target, namespace, {
240
+ ...options,
241
+ lng: options?.lng || language,
242
+ });
243
+ }
244
+ return Reflect.get(target, property, receiver);
245
+ },
246
+ });
183
247
  }
184
248
 
185
249
  export async function getI18nInstance(
186
- userInstance?: I18nInstance | any,
250
+ userInstance?: unknown,
187
251
  ): Promise<I18nInstance> {
188
252
  if (userInstance) {
189
253
  if (isI18nWrapperInstance(userInstance)) {
@@ -1,12 +1,7 @@
1
- import type React from 'react';
1
+ import type { ReactI18nextIntegration } from '../reactI18next';
2
2
 
3
3
  type ReactI18nextModule = typeof import('react-i18next');
4
4
 
5
- interface ReactI18nextIntegration {
6
- I18nextProvider: React.ComponentType<any> | null;
7
- initReactI18next: any | null;
8
- }
9
-
10
5
  async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
11
6
  try {
12
7
  return (await import('react-i18next')) as ReactI18nextModule;