@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.
- package/dist/cjs/cli/index.js +2 -1
- package/dist/cjs/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +18 -82
- package/dist/cjs/runtime/contextHelpers.js +137 -0
- package/dist/cjs/runtime/core.js +29 -132
- package/dist/cjs/runtime/i18n/detection/cache.js +149 -0
- package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
- package/dist/cjs/runtime/i18n/detection/index.js +8 -263
- package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
- package/dist/cjs/runtime/i18n/detection/language.js +62 -0
- package/dist/cjs/runtime/i18n/detection/path.js +49 -0
- package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
- package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
- package/dist/cjs/runtime/i18n/detection/types.js +18 -0
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +113 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -152
- package/dist/cjs/server/redirectPolicy.js +102 -0
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +75 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
- package/dist/cjs/shared/localisedUrls/routes.js +129 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +13 -294
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +18 -82
- package/dist/esm/runtime/contextHelpers.mjs +84 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/i18n/detection/cache.mjs +102 -0
- package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
- package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
- package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
- package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
- package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +75 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +4 -139
- package/dist/esm/server/redirectPolicy.mjs +52 -0
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +6 -0
- package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +50 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +18 -82
- package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -0
- package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
- package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
- package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
- package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
- package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +76 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- package/dist/esm-node/server/apiPrefix.mjs +32 -0
- package/dist/esm-node/server/detectorOptions.mjs +60 -0
- package/dist/esm-node/server/index.mjs +4 -139
- package/dist/esm-node/server/redirectPolicy.mjs +53 -0
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +7 -0
- package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +51 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +2 -2
- package/dist/types/runtime/contextHelpers.d.ts +28 -0
- package/dist/types/runtime/core.d.ts +3 -7
- package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
- package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
- package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
- package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/types.d.ts +29 -0
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- package/dist/types/runtime/linkHelpers.d.ts +17 -0
- package/dist/types/runtime/pluginSetup.d.ts +32 -0
- package/dist/types/runtime/providerComposition.d.ts +15 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- package/dist/types/server/apiPrefix.d.ts +7 -0
- package/dist/types/server/detectorOptions.d.ts +21 -0
- package/dist/types/server/index.d.ts +1 -6
- package/dist/types/server/redirectPolicy.d.ts +16 -0
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +7 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +8 -0
- package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
- package/dist/types/shared/localisedUrls/types.d.ts +14 -0
- package/dist/types/shared/localisedUrls.d.ts +2 -42
- package/package.json +10 -10
- package/src/cli/index.ts +3 -2
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +42 -196
- package/src/runtime/contextHelpers.ts +232 -0
- package/src/runtime/core.tsx +42 -273
- package/src/runtime/i18n/detection/cache.ts +175 -0
- package/src/runtime/i18n/detection/detector.ts +157 -0
- package/src/runtime/i18n/detection/index.ts +10 -641
- package/src/runtime/i18n/detection/initOptions.ts +74 -0
- package/src/runtime/i18n/detection/language.ts +64 -0
- package/src/runtime/i18n/detection/path.ts +41 -0
- package/src/runtime/i18n/detection/priority.ts +82 -0
- package/src/runtime/i18n/detection/ssr.ts +47 -0
- package/src/runtime/i18n/detection/types.ts +33 -0
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +142 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +12 -327
- package/src/server/redirectPolicy.ts +152 -0
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +13 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +57 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/resolve.ts +118 -0
- package/src/shared/localisedUrls/routes.ts +226 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +20 -629
- package/tests/link.test.tsx +111 -0
- package/tests/localisedUrls.test.ts +50 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +5 -2
- 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
|
|
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
|
+
}
|