@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.4 → 3.5.0-ultramodern.41

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 (214) 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 +18 -82
  5. package/dist/cjs/runtime/contextHelpers.js +137 -0
  6. package/dist/cjs/runtime/core.js +29 -132
  7. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  8. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  9. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  10. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  11. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  12. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  13. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  14. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  15. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  16. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  17. package/dist/cjs/runtime/i18n/instance.js +7 -7
  18. package/dist/cjs/runtime/linkHelpers.js +148 -0
  19. package/dist/cjs/runtime/pluginSetup.js +102 -0
  20. package/dist/cjs/runtime/providerComposition.js +114 -0
  21. package/dist/cjs/runtime/reactI18next.js +43 -0
  22. package/dist/cjs/runtime/utils.js +1 -9
  23. package/dist/cjs/server/apiPrefix.js +72 -0
  24. package/dist/cjs/server/detectorOptions.js +97 -0
  25. package/dist/cjs/server/index.js +17 -144
  26. package/dist/cjs/server/redirectPolicy.js +96 -0
  27. package/dist/cjs/shared/detection.js +2 -104
  28. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  29. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  30. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  31. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  32. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  33. package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
  34. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  35. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  36. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  37. package/dist/cjs/shared/localisedUrls.js +28 -294
  38. package/dist/cjs/shared/utils.js +2 -8
  39. package/dist/esm/cli/index.mjs +2 -1
  40. package/dist/esm/cli/locales.mjs +13 -13
  41. package/dist/esm/runtime/Link.mjs +1 -82
  42. package/dist/esm/runtime/context.mjs +18 -82
  43. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  44. package/dist/esm/runtime/core.mjs +30 -133
  45. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  46. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  47. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  48. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  49. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  50. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  51. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  52. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  54. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  55. package/dist/esm/runtime/i18n/instance.mjs +8 -5
  56. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  57. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  58. package/dist/esm/runtime/providerComposition.mjs +76 -0
  59. package/dist/esm/runtime/reactI18next.mjs +5 -0
  60. package/dist/esm/runtime/utils.mjs +2 -10
  61. package/dist/esm/server/apiPrefix.mjs +31 -0
  62. package/dist/esm/server/detectorOptions.mjs +59 -0
  63. package/dist/esm/server/index.mjs +8 -135
  64. package/dist/esm/server/redirectPolicy.mjs +46 -0
  65. package/dist/esm/shared/detection.mjs +1 -71
  66. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  67. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  68. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  69. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  70. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  71. package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
  72. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  73. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  74. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  75. package/dist/esm/shared/localisedUrls.mjs +1 -283
  76. package/dist/esm/shared/utils.mjs +1 -1
  77. package/dist/esm-node/cli/index.mjs +2 -1
  78. package/dist/esm-node/cli/locales.mjs +13 -13
  79. package/dist/esm-node/runtime/Link.mjs +1 -82
  80. package/dist/esm-node/runtime/context.mjs +18 -82
  81. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  82. package/dist/esm-node/runtime/core.mjs +30 -133
  83. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  84. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  85. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  86. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  87. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  88. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  89. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  90. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  91. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  92. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  93. package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
  94. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  95. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  96. package/dist/esm-node/runtime/providerComposition.mjs +77 -0
  97. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  98. package/dist/esm-node/runtime/utils.mjs +2 -10
  99. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  100. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  101. package/dist/esm-node/server/index.mjs +8 -135
  102. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  103. package/dist/esm-node/shared/detection.mjs +1 -71
  104. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  105. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  106. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  107. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  108. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  109. package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
  110. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  111. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  112. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  113. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  114. package/dist/esm-node/shared/utils.mjs +1 -1
  115. package/dist/types/cli/locales.d.ts +1 -1
  116. package/dist/types/runtime/Link.d.ts +3 -7
  117. package/dist/types/runtime/context.d.ts +6 -5
  118. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  119. package/dist/types/runtime/core.d.ts +3 -7
  120. package/dist/types/runtime/hooks.d.ts +5 -0
  121. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  122. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  123. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  124. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  125. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  126. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  127. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  128. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  129. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  130. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  131. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  132. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  133. package/dist/types/runtime/i18n/instance.d.ts +28 -26
  134. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  135. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  136. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  137. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  138. package/dist/types/runtime/providerComposition.d.ts +16 -0
  139. package/dist/types/runtime/reactI18next.d.ts +7 -0
  140. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  141. package/dist/types/runtime/utils.d.ts +7 -2
  142. package/dist/types/server/apiPrefix.d.ts +7 -0
  143. package/dist/types/server/detectorOptions.d.ts +21 -0
  144. package/dist/types/server/index.d.ts +1 -6
  145. package/dist/types/server/redirectPolicy.d.ts +22 -0
  146. package/dist/types/shared/detection.d.ts +1 -11
  147. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  148. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  149. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  150. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  151. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  152. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  153. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  154. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  155. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  156. package/dist/types/shared/localisedUrls.d.ts +2 -36
  157. package/dist/types/shared/utils.d.ts +0 -2
  158. package/package.json +16 -18
  159. package/rstest.config.mts +1 -0
  160. package/src/cli/index.ts +5 -4
  161. package/src/cli/locales.ts +3 -3
  162. package/src/runtime/Link.tsx +11 -138
  163. package/src/runtime/context.tsx +45 -199
  164. package/src/runtime/contextHelpers.ts +232 -0
  165. package/src/runtime/core.tsx +43 -274
  166. package/src/runtime/hooks.ts +13 -2
  167. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  168. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  169. package/src/runtime/i18n/detection/cache.ts +189 -0
  170. package/src/runtime/i18n/detection/detector.ts +166 -0
  171. package/src/runtime/i18n/detection/index.ts +10 -641
  172. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  173. package/src/runtime/i18n/detection/language.ts +64 -0
  174. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  175. package/src/runtime/i18n/detection/middleware.ts +15 -7
  176. package/src/runtime/i18n/detection/path.ts +41 -0
  177. package/src/runtime/i18n/detection/priority.ts +82 -0
  178. package/src/runtime/i18n/detection/ssr.ts +47 -0
  179. package/src/runtime/i18n/detection/types.ts +41 -0
  180. package/src/runtime/i18n/instance.ts +56 -31
  181. package/src/runtime/i18n/react-i18next.ts +1 -6
  182. package/src/runtime/i18n/utils.ts +18 -9
  183. package/src/runtime/linkHelpers.ts +174 -0
  184. package/src/runtime/pluginSetup.ts +189 -0
  185. package/src/runtime/providerComposition.tsx +144 -0
  186. package/src/runtime/reactI18next.ts +20 -0
  187. package/src/runtime/routerAdapter.tsx +6 -6
  188. package/src/runtime/utils.ts +13 -35
  189. package/src/server/apiPrefix.ts +67 -0
  190. package/src/server/detectorOptions.ts +105 -0
  191. package/src/server/index.ts +14 -319
  192. package/src/server/redirectPolicy.ts +127 -0
  193. package/src/shared/detection.ts +1 -1
  194. package/src/shared/localisedUrls/config.ts +18 -0
  195. package/src/shared/localisedUrls/index.ts +24 -0
  196. package/src/shared/localisedUrls/normalise.ts +67 -0
  197. package/src/shared/localisedUrls/pathname.ts +60 -0
  198. package/src/shared/localisedUrls/patterns.ts +160 -0
  199. package/src/shared/localisedUrls/redirect.ts +91 -0
  200. package/src/shared/localisedUrls/resolve.ts +135 -0
  201. package/src/shared/localisedUrls/routes.ts +226 -0
  202. package/src/shared/localisedUrls/types.ts +18 -0
  203. package/src/shared/localisedUrls.ts +26 -623
  204. package/src/shared/utils.ts +2 -2
  205. package/tests/fixtures/localised-routes.golden.json +156 -0
  206. package/tests/link.test.tsx +111 -0
  207. package/tests/linkTypes.test.ts +15 -4
  208. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  209. package/tests/localisedUrls.test.ts +129 -2
  210. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  211. package/tests/redirectPolicy.test.ts +43 -0
  212. package/tests/routerAdapter.test.tsx +85 -0
  213. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  214. 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,42 @@ 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
85
  cloneInstance?: () => 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>;
71
90
  services?: {
72
91
  languageDetector?: {
73
- detect: (request?: any, options?: any) => string | string[] | undefined;
74
- [key: string]: any;
92
+ detect: (
93
+ request?: unknown,
94
+ options?: unknown,
95
+ ) => string | string[] | undefined;
96
+ [key: string]: unknown;
75
97
  };
76
98
  resourceStore?: I18nResourceStore;
77
- backend?: any; // Backend instance (e.g., SdkBackend)
78
- [key: string]: any;
99
+ backend?: unknown; // Backend instance (e.g., SdkBackend)
100
+ [key: string]: unknown;
79
101
  };
80
102
  // i18next instance options (available after initialization)
81
103
  options?: {
82
104
  backend?: BackendOptions;
83
- [key: string]: any;
105
+ [key: string]: unknown;
84
106
  };
85
- [key: string]: any;
107
+ [key: string]: unknown;
86
108
  }
87
109
 
88
110
  type LanguageDetectorOrder = string[];
@@ -101,12 +123,12 @@ export interface LanguageDetectorOptions {
101
123
  }
102
124
 
103
125
  export interface BackendOptions extends Omit<BaseBackendOptions, 'enabled'> {
104
- parse?: (data: string) => any;
105
- stringify?: (data: any) => string;
126
+ parse?: (data: string) => unknown;
127
+ stringify?: (data: unknown) => string;
106
128
  [key: string]: any;
107
129
  }
108
130
 
109
- export type ResourceValue = string | { [key: string]: ResourceValue };
131
+ type ResourceValue = string | { [key: string]: ResourceValue };
110
132
 
111
133
  export interface Resources {
112
134
  [lng: string]: {
@@ -126,15 +148,15 @@ export type I18nInitOptions = {
126
148
  defaultNS?: string | string[];
127
149
  interpolation?: {
128
150
  escapeValue?: boolean;
129
- [key: string]: any;
151
+ [key: string]: unknown;
130
152
  };
131
153
  react?: {
132
154
  useSuspense?: boolean;
133
- [key: string]: any;
155
+ [key: string]: unknown;
134
156
  };
135
157
  };
136
158
 
137
- export function isI18nInstance(obj: any): obj is I18nInstance {
159
+ export function isI18nInstance(obj: unknown): obj is I18nInstance {
138
160
  if (!obj || typeof obj !== 'object') {
139
161
  return false;
140
162
  }
@@ -143,7 +165,10 @@ export function isI18nInstance(obj: any): obj is I18nInstance {
143
165
  return true;
144
166
  }
145
167
 
146
- return typeof obj.init === 'function' && typeof obj.use === 'function';
168
+ const candidate = obj as { init?: unknown; use?: unknown };
169
+ return (
170
+ typeof candidate.init === 'function' && typeof candidate.use === 'function'
171
+ );
147
172
  }
148
173
 
149
174
  async function tryImportI18next(): Promise<I18nInstance | null> {
@@ -170,8 +195,8 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
170
195
  }
171
196
 
172
197
  export function getI18nextInstanceForProvider(
173
- instance: I18nInstance | any,
174
- ): any {
198
+ instance: I18nInstance,
199
+ ): I18nInstance {
175
200
  if (isI18nWrapperInstance(instance)) {
176
201
  const i18nextInstance = getI18nWrapperI18nextInstance(instance);
177
202
  if (i18nextInstance) {
@@ -183,7 +208,7 @@ export function getI18nextInstanceForProvider(
183
208
  }
184
209
 
185
210
  export async function getI18nInstance(
186
- userInstance?: I18nInstance | any,
211
+ userInstance?: unknown,
187
212
  ): Promise<I18nInstance> {
188
213
  if (userInstance) {
189
214
  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;
@@ -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> => {