@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.
@@ -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 = (_detectionOptions)=>{};
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, Headers;
51
- return req.headers.get(name);
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: code.split('-')[0],
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)if (0 === languages.length || languages.includes(lang.code)) {
91
- detectedLang = lang.code;
92
- break;
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 = (_detectionOptions)=>{};
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
- 4734 (module) {
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__(4734);
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, Headers;
19
- return req.headers.get(name);
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: code.split('-')[0],
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)if (0 === languages.length || languages.includes(lang.code)) {
59
- detectedLang = lang.code;
60
- break;
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 = (_detectionOptions)=>{};
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, Headers;
20
- return req.headers.get(name);
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: code.split('-')[0],
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)if (0 === languages.length || languages.includes(lang.code)) {
60
- detectedLang = lang.code;
61
- break;
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 { I18nInstance } from '../instance.js';
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?: any) => string | undefined;
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 type { I18nInstance } from '../instance.js';
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
- } | Headers;
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.1",
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.7",
85
+ "i18next-fs-backend": "^2.6.8",
86
86
  "i18next-http-backend": "^3.0.6",
87
- "i18next-http-middleware": "^3.9.8",
88
- "@modern-js/plugin": "3.9.1",
89
- "@modern-js/runtime-utils": "3.9.1",
90
- "@modern-js/server-runtime": "3.9.1",
91
- "@modern-js/server-core": "3.9.1",
92
- "@modern-js/types": "3.9.1",
93
- "@modern-js/utils": "3.9.1"
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.1"
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.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.1",
119
+ "@modern-js/app-tools": "3.9.3",
120
120
  "@modern-js/rslib": "2.68.10",
121
- "@modern-js/runtime": "3.9.1"
121
+ "@modern-js/runtime": "3.9.3"
122
122
  },
123
123
  "sideEffects": false,
124
124
  "publishConfig": {
@@ -0,0 +1,7 @@
1
+ import { withTestPreset } from '@scripts/rstest-config';
2
+
3
+ export default withTestPreset({
4
+ root: __dirname,
5
+ testEnvironment: 'node',
6
+ globals: true,
7
+ });
@@ -538,6 +538,8 @@ export const detectLanguageWithPriority = async (
538
538
  localePathRedirect,
539
539
  userInitOptions,
540
540
  ),
541
+ ssrContext?.request,
542
+ languages,
541
543
  );
542
544
  } else {
543
545
  detectedLanguage = await detectLanguageFromI18nextDetector(i18nInstance, {
@@ -1,5 +1,6 @@
1
1
  import { LanguageDetector } from 'i18next-http-middleware';
2
- import type { I18nInstance } from '../instance';
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
- _detectionOptions?: any,
14
+ detectionOptions?: LanguageDetectorOptions,
15
+ request?: Parameters<typeof detectLanguageFromRequest>[0],
16
+ languages: string[] = [],
18
17
  ): string | undefined => {
19
- // In Node.js environment, storage-based detection is not available
20
- return undefined;
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 { I18nInstance } from '../instance';
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?: any,
31
+ detectionOptions?: LanguageDetectorOptions,
32
+ _request?: Parameters<typeof detectLanguageFromRequest>[0],
33
+ _languages?: string[],
31
34
  ): string | undefined => {
32
35
  try {
33
36
  const options = detectionOptions || {};
@@ -12,10 +12,9 @@ export function detectLanguageFromRequest(
12
12
  req: {
13
13
  url: string;
14
14
  headers:
15
- | {
16
- get: (name: string) => string | null;
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
- if (req.headers instanceof Headers) {
44
- return req.headers.get(name);
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 req.headers.get(name);
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: code.split('-')[0], // Extract base language 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
+ });