@modern-js/plugin-i18n 3.9.1 → 3.9.3
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/runtime/i18n/detection/index.js +1 -1
- package/dist/cjs/runtime/i18n/detection/middleware.js +1 -1
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +6 -1
- package/dist/cjs/shared/detection.js +20 -6
- package/dist/esm/runtime/i18n/detection/index.mjs +1 -1
- package/dist/esm/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm/runtime/i18n/detection/middleware.node.mjs +6 -1
- package/dist/esm/runtime/utils.mjs +2 -2
- package/dist/esm/shared/detection.mjs +20 -6
- package/dist/esm-node/runtime/i18n/detection/index.mjs +1 -1
- package/dist/esm-node/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm-node/runtime/i18n/detection/middleware.node.mjs +6 -1
- package/dist/esm-node/shared/detection.mjs +20 -6
- package/dist/types/runtime/i18n/detection/middleware.d.ts +3 -2
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +3 -6
- package/dist/types/shared/detection.d.ts +2 -2
- package/package.json +13 -13
- package/rstest.config.mts +7 -0
- package/src/runtime/i18n/detection/index.ts +2 -0
- package/src/runtime/i18n/detection/middleware.node.ts +15 -8
- package/src/runtime/i18n/detection/middleware.ts +5 -2
- package/src/shared/detection.ts +33 -9
- package/tests/detection.test.ts +37 -0
|
@@ -253,7 +253,7 @@ const detectLanguageWithPriority = async (i18nInstance, options)=>{
|
|
|
253
253
|
let detectedLanguage;
|
|
254
254
|
detectedLanguage = detectLanguageFromSSR(languages);
|
|
255
255
|
if (!detectedLanguage) detectedLanguage = detectLanguageFromPathPriority(pathname, languages, localePathRedirect);
|
|
256
|
-
if (!detectedLanguage && i18nextDetector) detectedLanguage = (0, external_instance_js_namespaceObject.isI18nWrapperInstance)(i18nInstance) ? (0, external_middleware_js_namespaceObject.readLanguageFromStorage)(mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions)) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
256
|
+
if (!detectedLanguage && i18nextDetector) detectedLanguage = (0, external_instance_js_namespaceObject.isI18nWrapperInstance)(i18nInstance) ? (0, external_middleware_js_namespaceObject.readLanguageFromStorage)(mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions), ssrContext?.request, languages) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
257
257
|
languages,
|
|
258
258
|
fallbackLanguage,
|
|
259
259
|
localePathRedirect,
|
|
@@ -56,7 +56,7 @@ const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
|
56
56
|
}
|
|
57
57
|
return i18nInstance;
|
|
58
58
|
};
|
|
59
|
-
const readLanguageFromStorage = (detectionOptions)=>{
|
|
59
|
+
const readLanguageFromStorage = (detectionOptions, _request, _languages)=>{
|
|
60
60
|
try {
|
|
61
61
|
const options = detectionOptions || {};
|
|
62
62
|
const order = options.order || [
|
|
@@ -34,8 +34,13 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
34
34
|
useI18nextLanguageDetector: ()=>useI18nextLanguageDetector
|
|
35
35
|
});
|
|
36
36
|
const external_i18next_http_middleware_namespaceObject = require("i18next-http-middleware");
|
|
37
|
+
const detection_js_namespaceObject = require("../../../shared/detection.js");
|
|
37
38
|
const cacheUserLanguage = (_i18nInstance, _language, _detectionOptions)=>{};
|
|
38
|
-
const readLanguageFromStorage = (
|
|
39
|
+
const readLanguageFromStorage = (detectionOptions, request, languages = [])=>{
|
|
40
|
+
if (!request) return;
|
|
41
|
+
const detectedLanguage = (0, detection_js_namespaceObject.detectLanguageFromRequest)(request, languages, detectionOptions);
|
|
42
|
+
return detectedLanguage ?? void 0;
|
|
43
|
+
};
|
|
39
44
|
const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
40
45
|
if (!i18nInstance.isInitialized) return i18nInstance.use(external_i18next_http_middleware_namespaceObject.LanguageDetector);
|
|
41
46
|
return i18nInstance;
|
|
@@ -47,8 +47,11 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
47
47
|
'header'
|
|
48
48
|
];
|
|
49
49
|
const getHeader = (name)=>{
|
|
50
|
-
req.headers
|
|
51
|
-
return
|
|
50
|
+
const headers = req.headers;
|
|
51
|
+
if ('function' == typeof headers?.get) return headers.get(name);
|
|
52
|
+
const value = headers[name.toLowerCase()];
|
|
53
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
54
|
+
return value ?? null;
|
|
52
55
|
};
|
|
53
56
|
for (const method of detectionOrder){
|
|
54
57
|
let detectedLang = null;
|
|
@@ -83,19 +86,30 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
83
86
|
const languagesList = acceptLanguage.split(',').map((lang)=>{
|
|
84
87
|
const [code, q] = lang.trim().split(';');
|
|
85
88
|
return {
|
|
86
|
-
code
|
|
89
|
+
code,
|
|
90
|
+
baseCode: code.split('-')[0],
|
|
87
91
|
quality: q ? parseFloat(q.split('=')[1]) : 1.0
|
|
88
92
|
};
|
|
89
93
|
}).sort((a, b)=>b.quality - a.quality);
|
|
90
|
-
for (const lang of languagesList)
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
for (const lang of languagesList){
|
|
95
|
+
if (0 === languages.length || languages.includes(lang.code)) {
|
|
96
|
+
detectedLang = lang.code;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (languages.includes(lang.baseCode)) {
|
|
100
|
+
detectedLang = lang.baseCode;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
break;
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
|
|
109
|
+
if (detectedLang) {
|
|
110
|
+
const baseLang = detectedLang.split('-')[0];
|
|
111
|
+
if (languages.includes(baseLang)) return baseLang;
|
|
112
|
+
}
|
|
99
113
|
}
|
|
100
114
|
} catch (error) {}
|
|
101
115
|
return null;
|
|
@@ -216,7 +216,7 @@ const detectLanguageWithPriority = async (i18nInstance, options)=>{
|
|
|
216
216
|
let detectedLanguage;
|
|
217
217
|
detectedLanguage = detectLanguageFromSSR(languages);
|
|
218
218
|
if (!detectedLanguage) detectedLanguage = detectLanguageFromPathPriority(pathname, languages, localePathRedirect);
|
|
219
|
-
if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(detection_mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions)) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
219
|
+
if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(detection_mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions), ssrContext?.request, languages) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
220
220
|
languages,
|
|
221
221
|
fallbackLanguage,
|
|
222
222
|
localePathRedirect,
|
|
@@ -11,7 +11,7 @@ const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
|
11
11
|
}
|
|
12
12
|
return i18nInstance;
|
|
13
13
|
};
|
|
14
|
-
const readLanguageFromStorage = (detectionOptions)=>{
|
|
14
|
+
const readLanguageFromStorage = (detectionOptions, _request, _languages)=>{
|
|
15
15
|
try {
|
|
16
16
|
const options = detectionOptions || {};
|
|
17
17
|
const order = options.order || [
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { LanguageDetector } from "i18next-http-middleware";
|
|
2
|
+
import { detectLanguageFromRequest } from "../../../shared/detection.mjs";
|
|
2
3
|
const cacheUserLanguage = (_i18nInstance, _language, _detectionOptions)=>{};
|
|
3
|
-
const readLanguageFromStorage = (
|
|
4
|
+
const readLanguageFromStorage = (detectionOptions, request, languages = [])=>{
|
|
5
|
+
if (!request) return;
|
|
6
|
+
const detectedLanguage = detectLanguageFromRequest(request, languages, detectionOptions);
|
|
7
|
+
return detectedLanguage ?? void 0;
|
|
8
|
+
};
|
|
4
9
|
const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
5
10
|
if (!i18nInstance.isInitialized) return i18nInstance.use(LanguageDetector);
|
|
6
11
|
return i18nInstance;
|
|
@@ -2,7 +2,7 @@ import { isBrowser } from "@modern-js/runtime";
|
|
|
2
2
|
import { getGlobalBasename } from "@modern-js/runtime/context";
|
|
3
3
|
import { __webpack_require__ } from "../rslib-runtime~1.mjs";
|
|
4
4
|
__webpack_require__.add({
|
|
5
|
-
|
|
5
|
+
He (module) {
|
|
6
6
|
module.exports = require("@modern-js/runtime/router");
|
|
7
7
|
}
|
|
8
8
|
});
|
|
@@ -55,7 +55,7 @@ const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes)=>{
|
|
|
55
55
|
};
|
|
56
56
|
const useRouterHooks = ()=>{
|
|
57
57
|
try {
|
|
58
|
-
const { useLocation, useNavigate, useParams } = __webpack_require__(
|
|
58
|
+
const { useLocation, useNavigate, useParams } = __webpack_require__("He");
|
|
59
59
|
return {
|
|
60
60
|
navigate: useNavigate(),
|
|
61
61
|
location: useLocation(),
|
|
@@ -15,8 +15,11 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
15
15
|
'header'
|
|
16
16
|
];
|
|
17
17
|
const getHeader = (name)=>{
|
|
18
|
-
req.headers
|
|
19
|
-
return
|
|
18
|
+
const headers = req.headers;
|
|
19
|
+
if ('function' == typeof headers?.get) return headers.get(name);
|
|
20
|
+
const value = headers[name.toLowerCase()];
|
|
21
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
22
|
+
return value ?? null;
|
|
20
23
|
};
|
|
21
24
|
for (const method of detectionOrder){
|
|
22
25
|
let detectedLang = null;
|
|
@@ -51,19 +54,30 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
51
54
|
const languagesList = acceptLanguage.split(',').map((lang)=>{
|
|
52
55
|
const [code, q] = lang.trim().split(';');
|
|
53
56
|
return {
|
|
54
|
-
code
|
|
57
|
+
code,
|
|
58
|
+
baseCode: code.split('-')[0],
|
|
55
59
|
quality: q ? parseFloat(q.split('=')[1]) : 1.0
|
|
56
60
|
};
|
|
57
61
|
}).sort((a, b)=>b.quality - a.quality);
|
|
58
|
-
for (const lang of languagesList)
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
for (const lang of languagesList){
|
|
63
|
+
if (0 === languages.length || languages.includes(lang.code)) {
|
|
64
|
+
detectedLang = lang.code;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (languages.includes(lang.baseCode)) {
|
|
68
|
+
detectedLang = lang.baseCode;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
}
|
|
63
73
|
break;
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
|
|
77
|
+
if (detectedLang) {
|
|
78
|
+
const baseLang = detectedLang.split('-')[0];
|
|
79
|
+
if (languages.includes(baseLang)) return baseLang;
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
} catch (error) {}
|
|
69
83
|
return null;
|
|
@@ -217,7 +217,7 @@ const detectLanguageWithPriority = async (i18nInstance, options)=>{
|
|
|
217
217
|
let detectedLanguage;
|
|
218
218
|
detectedLanguage = detectLanguageFromSSR(languages);
|
|
219
219
|
if (!detectedLanguage) detectedLanguage = detectLanguageFromPathPriority(pathname, languages, localePathRedirect);
|
|
220
|
-
if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(detection_mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions)) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
220
|
+
if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(detection_mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions), ssrContext?.request, languages) : await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
221
221
|
languages,
|
|
222
222
|
fallbackLanguage,
|
|
223
223
|
localePathRedirect,
|
|
@@ -12,7 +12,7 @@ const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
|
12
12
|
}
|
|
13
13
|
return i18nInstance;
|
|
14
14
|
};
|
|
15
|
-
const readLanguageFromStorage = (detectionOptions)=>{
|
|
15
|
+
const readLanguageFromStorage = (detectionOptions, _request, _languages)=>{
|
|
16
16
|
try {
|
|
17
17
|
const options = detectionOptions || {};
|
|
18
18
|
const order = options.order || [
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { LanguageDetector } from "i18next-http-middleware";
|
|
3
|
+
import { detectLanguageFromRequest } from "../../../shared/detection.mjs";
|
|
3
4
|
const cacheUserLanguage = (_i18nInstance, _language, _detectionOptions)=>{};
|
|
4
|
-
const readLanguageFromStorage = (
|
|
5
|
+
const readLanguageFromStorage = (detectionOptions, request, languages = [])=>{
|
|
6
|
+
if (!request) return;
|
|
7
|
+
const detectedLanguage = detectLanguageFromRequest(request, languages, detectionOptions);
|
|
8
|
+
return detectedLanguage ?? void 0;
|
|
9
|
+
};
|
|
5
10
|
const useI18nextLanguageDetector = (i18nInstance)=>{
|
|
6
11
|
if (!i18nInstance.isInitialized) return i18nInstance.use(LanguageDetector);
|
|
7
12
|
return i18nInstance;
|
|
@@ -16,8 +16,11 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
16
16
|
'header'
|
|
17
17
|
];
|
|
18
18
|
const getHeader = (name)=>{
|
|
19
|
-
req.headers
|
|
20
|
-
return
|
|
19
|
+
const headers = req.headers;
|
|
20
|
+
if ('function' == typeof headers?.get) return headers.get(name);
|
|
21
|
+
const value = headers[name.toLowerCase()];
|
|
22
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
23
|
+
return value ?? null;
|
|
21
24
|
};
|
|
22
25
|
for (const method of detectionOrder){
|
|
23
26
|
let detectedLang = null;
|
|
@@ -52,19 +55,30 @@ function detectLanguageFromRequest(req, languages, detectionOptions) {
|
|
|
52
55
|
const languagesList = acceptLanguage.split(',').map((lang)=>{
|
|
53
56
|
const [code, q] = lang.trim().split(';');
|
|
54
57
|
return {
|
|
55
|
-
code
|
|
58
|
+
code,
|
|
59
|
+
baseCode: code.split('-')[0],
|
|
56
60
|
quality: q ? parseFloat(q.split('=')[1]) : 1.0
|
|
57
61
|
};
|
|
58
62
|
}).sort((a, b)=>b.quality - a.quality);
|
|
59
|
-
for (const lang of languagesList)
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
for (const lang of languagesList){
|
|
64
|
+
if (0 === languages.length || languages.includes(lang.code)) {
|
|
65
|
+
detectedLang = lang.code;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (languages.includes(lang.baseCode)) {
|
|
69
|
+
detectedLang = lang.baseCode;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
break;
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
|
|
78
|
+
if (detectedLang) {
|
|
79
|
+
const baseLang = detectedLang.split('-')[0];
|
|
80
|
+
if (languages.includes(baseLang)) return baseLang;
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
} catch (error) {}
|
|
70
84
|
return null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { detectLanguageFromRequest } from '../../../shared/detection.js';
|
|
2
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance.js';
|
|
2
3
|
/**
|
|
3
4
|
* Register LanguageDetector plugin to i18n instance
|
|
4
5
|
* Must be called before init() to properly register the detector
|
|
@@ -9,7 +10,7 @@ export declare const useI18nextLanguageDetector: (i18nInstance: I18nInstance) =>
|
|
|
9
10
|
* Read language directly from localStorage/cookie
|
|
10
11
|
* Fallback when detector is not available in services
|
|
11
12
|
*/
|
|
12
|
-
export declare const readLanguageFromStorage: (detectionOptions?:
|
|
13
|
+
export declare const readLanguageFromStorage: (detectionOptions?: LanguageDetectorOptions, _request?: Parameters<typeof detectLanguageFromRequest>[0], _languages?: string[]) => string | undefined;
|
|
13
14
|
/**
|
|
14
15
|
* Detect language using i18next-browser-languagedetector
|
|
15
16
|
* For initialized instances without detector in services, manually create a detector instance
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { detectLanguageFromRequest } from '../../../shared/detection.js';
|
|
2
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance.js';
|
|
2
3
|
export declare const cacheUserLanguage: (_i18nInstance: I18nInstance, _language: string, _detectionOptions?: any) => void;
|
|
3
|
-
|
|
4
|
-
* Read language directly from storage (localStorage/cookie)
|
|
5
|
-
* Not available in Node.js environment, returns undefined
|
|
6
|
-
*/
|
|
7
|
-
export declare const readLanguageFromStorage: (_detectionOptions?: any) => string | undefined;
|
|
4
|
+
export declare const readLanguageFromStorage: (detectionOptions?: LanguageDetectorOptions, request?: Parameters<typeof detectLanguageFromRequest>[0], languages?: string[]) => string | undefined;
|
|
8
5
|
/**
|
|
9
6
|
* Register LanguageDetector plugin to i18n instance
|
|
10
7
|
* Must be called before init() to properly register the detector
|
|
@@ -5,7 +5,7 @@ import type { LanguageDetectorOptions } from '../runtime/i18n/instance.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function detectLanguageFromRequest(req: {
|
|
7
7
|
url: string;
|
|
8
|
-
headers: {
|
|
8
|
+
headers: Headers | {
|
|
9
9
|
get: (name: string) => string | null;
|
|
10
|
-
} |
|
|
10
|
+
} | Record<string, string | string[] | undefined>;
|
|
11
11
|
}, languages: string[], detectionOptions?: LanguageDetectorOptions): string | null;
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"modern",
|
|
16
16
|
"modern.js"
|
|
17
17
|
],
|
|
18
|
-
"version": "3.9.
|
|
18
|
+
"version": "3.9.3",
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
@@ -82,22 +82,22 @@
|
|
|
82
82
|
"@swc/helpers": "^0.5.17",
|
|
83
83
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
84
84
|
"i18next-chained-backend": "^4.6.3",
|
|
85
|
-
"i18next-fs-backend": "^2.6.
|
|
85
|
+
"i18next-fs-backend": "^2.6.8",
|
|
86
86
|
"i18next-http-backend": "^3.0.6",
|
|
87
|
-
"i18next-http-middleware": "^3.9.
|
|
88
|
-
"@modern-js/plugin": "3.9.
|
|
89
|
-
"@modern-js/
|
|
90
|
-
"@modern-js/
|
|
91
|
-
"@modern-js/server-
|
|
92
|
-
"@modern-js/types": "3.9.
|
|
93
|
-
"@modern-js/utils": "3.9.
|
|
87
|
+
"i18next-http-middleware": "^3.9.9",
|
|
88
|
+
"@modern-js/plugin": "3.9.3",
|
|
89
|
+
"@modern-js/server-core": "3.9.3",
|
|
90
|
+
"@modern-js/runtime-utils": "3.9.3",
|
|
91
|
+
"@modern-js/server-runtime": "3.9.3",
|
|
92
|
+
"@modern-js/types": "3.9.3",
|
|
93
|
+
"@modern-js/utils": "3.9.3"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=18.3.1",
|
|
97
97
|
"react-dom": ">=18.3.1",
|
|
98
98
|
"i18next": ">=25.7.4",
|
|
99
99
|
"react-i18next": ">=15.7.4",
|
|
100
|
-
"@modern-js/runtime": "^3.9.
|
|
100
|
+
"@modern-js/runtime": "^3.9.2"
|
|
101
101
|
},
|
|
102
102
|
"peerDependenciesMeta": {
|
|
103
103
|
"i18next": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@rslib/core": "1.0.
|
|
111
|
+
"@rslib/core": "1.0.1",
|
|
112
112
|
"@types/node": "^20",
|
|
113
113
|
"i18next": "25.7.4",
|
|
114
114
|
"react": "^19.2.8",
|
|
@@ -116,9 +116,9 @@
|
|
|
116
116
|
"react-i18next": "15.7.4",
|
|
117
117
|
"ts-node": "^10.9.2",
|
|
118
118
|
"typescript": "^5",
|
|
119
|
-
"@modern-js/app-tools": "3.9.
|
|
119
|
+
"@modern-js/app-tools": "3.9.3",
|
|
120
120
|
"@modern-js/rslib": "2.68.10",
|
|
121
|
-
"@modern-js/runtime": "3.9.
|
|
121
|
+
"@modern-js/runtime": "3.9.3"
|
|
122
122
|
},
|
|
123
123
|
"sideEffects": false,
|
|
124
124
|
"publishConfig": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LanguageDetector } from 'i18next-http-middleware';
|
|
2
|
-
import
|
|
2
|
+
import { detectLanguageFromRequest } from '../../../shared/detection.js';
|
|
3
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance';
|
|
3
4
|
|
|
4
5
|
export const cacheUserLanguage = (
|
|
5
6
|
_i18nInstance: I18nInstance,
|
|
@@ -9,15 +10,21 @@ export const cacheUserLanguage = (
|
|
|
9
10
|
return;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Read language directly from storage (localStorage/cookie)
|
|
14
|
-
* Not available in Node.js environment, returns undefined
|
|
15
|
-
*/
|
|
16
13
|
export const readLanguageFromStorage = (
|
|
17
|
-
|
|
14
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
15
|
+
request?: Parameters<typeof detectLanguageFromRequest>[0],
|
|
16
|
+
languages: string[] = [],
|
|
18
17
|
): string | undefined => {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (!request) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const detectedLanguage = detectLanguageFromRequest(
|
|
23
|
+
request,
|
|
24
|
+
languages,
|
|
25
|
+
detectionOptions,
|
|
26
|
+
);
|
|
27
|
+
return detectedLanguage ?? undefined;
|
|
21
28
|
};
|
|
22
29
|
/**
|
|
23
30
|
* Register LanguageDetector plugin to i18n instance
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
3
|
-
import type {
|
|
3
|
+
import type { detectLanguageFromRequest } from '../../../shared/detection.js';
|
|
4
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance';
|
|
4
5
|
import { getActualI18nextInstance, isI18nWrapperInstance } from '../instance';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -27,7 +28,9 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
|
|
|
27
28
|
* Fallback when detector is not available in services
|
|
28
29
|
*/
|
|
29
30
|
export const readLanguageFromStorage = (
|
|
30
|
-
detectionOptions?:
|
|
31
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
32
|
+
_request?: Parameters<typeof detectLanguageFromRequest>[0],
|
|
33
|
+
_languages?: string[],
|
|
31
34
|
): string | undefined => {
|
|
32
35
|
try {
|
|
33
36
|
const options = detectionOptions || {};
|
package/src/shared/detection.ts
CHANGED
|
@@ -12,10 +12,9 @@ export function detectLanguageFromRequest(
|
|
|
12
12
|
req: {
|
|
13
13
|
url: string;
|
|
14
14
|
headers:
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| Headers;
|
|
15
|
+
| Headers
|
|
16
|
+
| { get: (name: string) => string | null }
|
|
17
|
+
| Record<string, string | string[] | undefined>;
|
|
19
18
|
},
|
|
20
19
|
languages: string[],
|
|
21
20
|
detectionOptions?: LanguageDetectorOptions,
|
|
@@ -39,11 +38,21 @@ export function detectLanguageFromRequest(
|
|
|
39
38
|
order.length > 0 ? order : ['querystring', 'cookie', 'header'];
|
|
40
39
|
|
|
41
40
|
// Helper to get header value
|
|
41
|
+
// req.headers is a Fetch API Headers instance when the caller passes the raw
|
|
42
|
+
// Request (e.g. ssrContext.request.raw), but modern.js's own SSR request object
|
|
43
|
+
// (ssrContext.request) carries headers as a plain, lower-cased key/value object.
|
|
42
44
|
const getHeader = (name: string): string | null => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const headers = req.headers;
|
|
46
|
+
if (typeof (headers as Headers)?.get === 'function') {
|
|
47
|
+
return (headers as Headers).get(name);
|
|
48
|
+
}
|
|
49
|
+
const value = (headers as Record<string, string | string[] | undefined>)[
|
|
50
|
+
name.toLowerCase()
|
|
51
|
+
];
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value[0] ?? null;
|
|
45
54
|
}
|
|
46
|
-
return
|
|
55
|
+
return value ?? null;
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
// Try each detection method in order
|
|
@@ -94,7 +103,8 @@ export function detectLanguageFromRequest(
|
|
|
94
103
|
.map((lang: string) => {
|
|
95
104
|
const [code, q] = lang.trim().split(';');
|
|
96
105
|
return {
|
|
97
|
-
code
|
|
106
|
+
code,
|
|
107
|
+
baseCode: code.split('-')[0],
|
|
98
108
|
quality: q ? parseFloat(q.split('=')[1]) : 1.0,
|
|
99
109
|
};
|
|
100
110
|
})
|
|
@@ -103,12 +113,17 @@ export function detectLanguageFromRequest(
|
|
|
103
113
|
b.quality - a.quality,
|
|
104
114
|
);
|
|
105
115
|
|
|
106
|
-
// Find first matching language
|
|
116
|
+
// Find first matching language, preferring an exact tag match
|
|
117
|
+
// (e.g. 'zh-Hant-TW') over the base language code (e.g. 'zh')
|
|
107
118
|
for (const lang of languagesList) {
|
|
108
119
|
if (languages.length === 0 || languages.includes(lang.code)) {
|
|
109
120
|
detectedLang = lang.code;
|
|
110
121
|
break;
|
|
111
122
|
}
|
|
123
|
+
if (languages.includes(lang.baseCode)) {
|
|
124
|
+
detectedLang = lang.baseCode;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
break;
|
|
@@ -122,6 +137,15 @@ export function detectLanguageFromRequest(
|
|
|
122
137
|
) {
|
|
123
138
|
return detectedLang;
|
|
124
139
|
}
|
|
140
|
+
|
|
141
|
+
// Match query and cookie values like header values: exact tag first,
|
|
142
|
+
// then the supported base language (e.g. 'zh-CN' -> 'zh').
|
|
143
|
+
if (detectedLang) {
|
|
144
|
+
const baseLang = detectedLang.split('-')[0];
|
|
145
|
+
if (languages.includes(baseLang)) {
|
|
146
|
+
return baseLang;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
125
149
|
}
|
|
126
150
|
} catch (error) {
|
|
127
151
|
// Silently ignore errors
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { detectLanguageFromRequest } from '../src/shared/detection';
|
|
2
|
+
|
|
3
|
+
// A later English header must not override a supported query/cookie language.
|
|
4
|
+
const requests = [
|
|
5
|
+
{
|
|
6
|
+
source: 'query',
|
|
7
|
+
url: '/?lng=zh-Hant-TW',
|
|
8
|
+
headers: { 'accept-language': 'en' },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
source: 'cookie',
|
|
12
|
+
url: '/',
|
|
13
|
+
headers: { cookie: 'i18next=zh-Hant-TW', 'accept-language': 'en' },
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
test.each(requests)('$source falls back to a supported base language', req => {
|
|
18
|
+
expect(detectLanguageFromRequest(req, ['en', 'zh'])).toBe('zh');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test.each(requests)(
|
|
22
|
+
'$source prefers the full tag when both are supported',
|
|
23
|
+
req => {
|
|
24
|
+
expect(detectLanguageFromRequest(req, ['en', 'zh', 'zh-Hant-TW'])).toBe(
|
|
25
|
+
'zh-Hant-TW',
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
test('skips an unsupported query language and matches the cookie base language', () => {
|
|
31
|
+
expect(
|
|
32
|
+
detectLanguageFromRequest(
|
|
33
|
+
{ url: '/?lng=fr-FR', headers: { cookie: 'i18next=zh-CN' } },
|
|
34
|
+
['en', 'zh'],
|
|
35
|
+
),
|
|
36
|
+
).toBe('zh');
|
|
37
|
+
});
|