@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.30 → 3.5.0-ultramodern.31

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 (161) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/runtime/Link.js +8 -89
  3. package/dist/cjs/runtime/context.js +18 -82
  4. package/dist/cjs/runtime/contextHelpers.js +137 -0
  5. package/dist/cjs/runtime/core.js +29 -132
  6. package/dist/cjs/runtime/i18n/detection/cache.js +149 -0
  7. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  8. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  9. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  10. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  11. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  12. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  13. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  14. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  15. package/dist/cjs/runtime/linkHelpers.js +148 -0
  16. package/dist/cjs/runtime/pluginSetup.js +102 -0
  17. package/dist/cjs/runtime/providerComposition.js +113 -0
  18. package/dist/cjs/runtime/reactI18next.js +43 -0
  19. package/dist/cjs/server/apiPrefix.js +72 -0
  20. package/dist/cjs/server/detectorOptions.js +97 -0
  21. package/dist/cjs/server/index.js +17 -152
  22. package/dist/cjs/server/redirectPolicy.js +102 -0
  23. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  24. package/dist/cjs/shared/localisedUrls/index.js +75 -0
  25. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  26. package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
  27. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  28. package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
  29. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  30. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  31. package/dist/cjs/shared/localisedUrls.js +13 -294
  32. package/dist/esm/cli/index.mjs +2 -1
  33. package/dist/esm/runtime/Link.mjs +1 -82
  34. package/dist/esm/runtime/context.mjs +18 -82
  35. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  36. package/dist/esm/runtime/core.mjs +30 -133
  37. package/dist/esm/runtime/i18n/detection/cache.mjs +102 -0
  38. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  39. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  40. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  41. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  42. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  43. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  44. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  45. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  46. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  47. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  48. package/dist/esm/runtime/providerComposition.mjs +75 -0
  49. package/dist/esm/runtime/reactI18next.mjs +5 -0
  50. package/dist/esm/server/apiPrefix.mjs +31 -0
  51. package/dist/esm/server/detectorOptions.mjs +59 -0
  52. package/dist/esm/server/index.mjs +4 -139
  53. package/dist/esm/server/redirectPolicy.mjs +52 -0
  54. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  55. package/dist/esm/shared/localisedUrls/index.mjs +6 -0
  56. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  57. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  58. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  59. package/dist/esm/shared/localisedUrls/resolve.mjs +50 -0
  60. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  61. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  62. package/dist/esm/shared/localisedUrls.mjs +1 -283
  63. package/dist/esm-node/cli/index.mjs +2 -1
  64. package/dist/esm-node/runtime/Link.mjs +1 -82
  65. package/dist/esm-node/runtime/context.mjs +18 -82
  66. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  67. package/dist/esm-node/runtime/core.mjs +30 -133
  68. package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -0
  69. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  70. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  71. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  72. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  73. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  74. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  75. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  76. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  77. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  78. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  79. package/dist/esm-node/runtime/providerComposition.mjs +76 -0
  80. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  81. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  82. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  83. package/dist/esm-node/server/index.mjs +4 -139
  84. package/dist/esm-node/server/redirectPolicy.mjs +53 -0
  85. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  86. package/dist/esm-node/shared/localisedUrls/index.mjs +7 -0
  87. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  88. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  89. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  90. package/dist/esm-node/shared/localisedUrls/resolve.mjs +51 -0
  91. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  92. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  93. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  94. package/dist/types/runtime/Link.d.ts +3 -7
  95. package/dist/types/runtime/context.d.ts +2 -2
  96. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  97. package/dist/types/runtime/core.d.ts +3 -7
  98. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  99. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  100. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  101. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  102. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  103. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  104. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  105. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  106. package/dist/types/runtime/i18n/detection/types.d.ts +29 -0
  107. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  108. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  109. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  110. package/dist/types/runtime/providerComposition.d.ts +15 -0
  111. package/dist/types/runtime/reactI18next.d.ts +7 -0
  112. package/dist/types/server/apiPrefix.d.ts +7 -0
  113. package/dist/types/server/detectorOptions.d.ts +21 -0
  114. package/dist/types/server/index.d.ts +1 -6
  115. package/dist/types/server/redirectPolicy.d.ts +16 -0
  116. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  117. package/dist/types/shared/localisedUrls/index.d.ts +7 -0
  118. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  119. package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
  120. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  121. package/dist/types/shared/localisedUrls/resolve.d.ts +8 -0
  122. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  123. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  124. package/dist/types/shared/localisedUrls.d.ts +2 -42
  125. package/package.json +10 -10
  126. package/src/cli/index.ts +3 -2
  127. package/src/runtime/Link.tsx +11 -138
  128. package/src/runtime/context.tsx +42 -196
  129. package/src/runtime/contextHelpers.ts +232 -0
  130. package/src/runtime/core.tsx +42 -273
  131. package/src/runtime/i18n/detection/cache.ts +175 -0
  132. package/src/runtime/i18n/detection/detector.ts +157 -0
  133. package/src/runtime/i18n/detection/index.ts +10 -641
  134. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  135. package/src/runtime/i18n/detection/language.ts +64 -0
  136. package/src/runtime/i18n/detection/path.ts +41 -0
  137. package/src/runtime/i18n/detection/priority.ts +82 -0
  138. package/src/runtime/i18n/detection/ssr.ts +47 -0
  139. package/src/runtime/i18n/detection/types.ts +33 -0
  140. package/src/runtime/i18n/react-i18next.ts +1 -6
  141. package/src/runtime/linkHelpers.ts +174 -0
  142. package/src/runtime/pluginSetup.ts +189 -0
  143. package/src/runtime/providerComposition.tsx +142 -0
  144. package/src/runtime/reactI18next.ts +20 -0
  145. package/src/server/apiPrefix.ts +67 -0
  146. package/src/server/detectorOptions.ts +105 -0
  147. package/src/server/index.ts +12 -327
  148. package/src/server/redirectPolicy.ts +152 -0
  149. package/src/shared/localisedUrls/config.ts +18 -0
  150. package/src/shared/localisedUrls/index.ts +13 -0
  151. package/src/shared/localisedUrls/normalise.ts +67 -0
  152. package/src/shared/localisedUrls/pathname.ts +57 -0
  153. package/src/shared/localisedUrls/patterns.ts +160 -0
  154. package/src/shared/localisedUrls/resolve.ts +118 -0
  155. package/src/shared/localisedUrls/routes.ts +226 -0
  156. package/src/shared/localisedUrls/types.ts +18 -0
  157. package/src/shared/localisedUrls.ts +20 -629
  158. package/tests/link.test.tsx +111 -0
  159. package/tests/localisedUrls.test.ts +50 -0
  160. package/tests/reactI18nextRuntimeBoundary.test.ts +5 -2
  161. package/tests/routerAdapter.test.tsx +56 -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
+ };
@@ -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,33 @@
1
+ import type { I18nInitOptions, LanguageDetectorOptions } from '../instance';
2
+
3
+ export interface BaseLanguageDetectionOptions {
4
+ languages: string[];
5
+ fallbackLanguage: string;
6
+ localePathRedirect: boolean;
7
+ i18nextDetector: boolean;
8
+ detection?: LanguageDetectorOptions;
9
+ userInitOptions?: I18nInitOptions;
10
+ mergedBackend?: any;
11
+ }
12
+
13
+ export interface LanguageDetectionOptions extends BaseLanguageDetectionOptions {
14
+ pathname: string;
15
+ ssrContext?: any;
16
+ }
17
+
18
+ export interface LanguageDetectionResult {
19
+ detectedLanguage?: string;
20
+ finalLanguage: string;
21
+ }
22
+
23
+ /**
24
+ * Options for building i18n init options
25
+ */
26
+ export interface BuildInitOptionsParams {
27
+ finalLanguage: string;
28
+ fallbackLanguage: string;
29
+ languages: string[];
30
+ userInitOptions?: I18nInitOptions;
31
+ mergedDetection?: any;
32
+ mergeBackend?: any;
33
+ }
@@ -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;
@@ -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
+ export 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
+ }