@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.35 → 3.5.0-ultramodern.37
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/locales.js +15 -15
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
- package/dist/cjs/runtime/i18n/detection/cache.js +3 -2
- package/dist/cjs/runtime/i18n/instance.js +7 -7
- package/dist/cjs/runtime/utils.js +1 -9
- package/dist/cjs/server/redirectPolicy.js +14 -20
- package/dist/cjs/shared/detection.js +2 -104
- package/dist/cjs/shared/localisedUrls/index.js +16 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +5 -2
- package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +29 -32
- package/dist/cjs/shared/localisedUrls.js +15 -0
- package/dist/cjs/shared/utils.js +2 -8
- package/dist/esm/cli/locales.mjs +13 -13
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm/runtime/i18n/detection/cache.mjs +3 -2
- package/dist/esm/runtime/i18n/instance.mjs +8 -5
- package/dist/esm/runtime/utils.mjs +2 -10
- package/dist/esm/server/index.mjs +2 -2
- package/dist/esm/server/redirectPolicy.mjs +15 -21
- package/dist/esm/shared/detection.mjs +1 -71
- package/dist/esm/shared/localisedUrls/index.mjs +2 -1
- package/dist/esm/shared/localisedUrls/pathname.mjs +1 -1
- package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +29 -32
- package/dist/esm/shared/localisedUrls.mjs +1 -1
- package/dist/esm/shared/utils.mjs +1 -1
- package/dist/esm-node/cli/locales.mjs +13 -13
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +3 -2
- package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
- package/dist/esm-node/runtime/utils.mjs +2 -10
- package/dist/esm-node/server/index.mjs +2 -2
- package/dist/esm-node/server/redirectPolicy.mjs +15 -21
- package/dist/esm-node/shared/detection.mjs +1 -71
- package/dist/esm-node/shared/localisedUrls/index.mjs +2 -1
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +1 -1
- package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +29 -32
- package/dist/esm-node/shared/localisedUrls.mjs +1 -1
- package/dist/esm-node/shared/utils.mjs +1 -1
- package/dist/types/cli/locales.d.ts +1 -1
- package/dist/types/runtime/context.d.ts +4 -3
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/cache.d.ts +1 -1
- package/dist/types/runtime/i18n/detection/detector.d.ts +2 -2
- package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/types.d.ts +8 -5
- package/dist/types/runtime/i18n/instance.d.ts +28 -26
- package/dist/types/runtime/i18n/utils.d.ts +14 -5
- package/dist/types/runtime/pluginSetup.d.ts +1 -1
- package/dist/types/runtime/providerComposition.d.ts +2 -1
- package/dist/types/runtime/routerAdapter.d.ts +7 -6
- package/dist/types/server/redirectPolicy.d.ts +14 -8
- package/dist/types/shared/detection.d.ts +1 -11
- package/dist/types/shared/localisedUrls/index.d.ts +3 -1
- package/dist/types/shared/localisedUrls/pathname.d.ts +1 -0
- package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +2 -3
- package/dist/types/shared/localisedUrls.d.ts +2 -2
- package/dist/types/shared/utils.d.ts +0 -2
- package/package.json +16 -18
- package/src/cli/index.ts +2 -2
- package/src/cli/locales.ts +3 -3
- package/src/runtime/context.tsx +3 -3
- package/src/runtime/contextHelpers.ts +3 -3
- package/src/runtime/hooks.ts +10 -2
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/middleware.common.ts +5 -4
- package/src/runtime/i18n/detection/cache.ts +23 -9
- package/src/runtime/i18n/detection/detector.ts +15 -6
- package/src/runtime/i18n/detection/middleware.node.ts +26 -7
- package/src/runtime/i18n/detection/middleware.ts +15 -7
- package/src/runtime/i18n/detection/types.ts +13 -5
- package/src/runtime/i18n/instance.ts +56 -31
- package/src/runtime/i18n/utils.ts +18 -9
- package/src/runtime/pluginSetup.ts +1 -1
- package/src/runtime/providerComposition.tsx +1 -1
- package/src/runtime/routerAdapter.tsx +6 -6
- package/src/runtime/utils.ts +5 -29
- package/src/server/redirectPolicy.ts +51 -76
- package/src/shared/detection.ts +1 -1
- package/src/shared/localisedUrls/index.ts +12 -1
- package/src/shared/localisedUrls/pathname.ts +4 -1
- package/src/shared/localisedUrls/redirect.ts +91 -0
- package/src/shared/localisedUrls/resolve.ts +91 -74
- package/src/shared/localisedUrls.ts +6 -0
- package/src/shared/utils.ts +2 -2
- package/tests/redirectPolicy.test.ts +43 -0
|
@@ -14,7 +14,10 @@ import { getSupportedLanguage, isLanguageSupported } from './language';
|
|
|
14
14
|
|
|
15
15
|
import { detectLanguage, useI18nextLanguageDetector } from './middleware';
|
|
16
16
|
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
BaseLanguageDetectionOptions,
|
|
19
|
+
LanguageDetectionSsrContext,
|
|
20
|
+
} from './types';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Initialize i18n instance for detector if needed
|
|
@@ -24,6 +27,10 @@ interface DetectorInitResult {
|
|
|
24
27
|
isTemporary: boolean;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
type MutableDetectorInstanceState = Omit<I18nInstance, 'language'> & {
|
|
31
|
+
language?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
27
34
|
const initializeI18nForDetector = async (
|
|
28
35
|
i18nInstance: I18nInstance,
|
|
29
36
|
options: BaseLanguageDetectionOptions,
|
|
@@ -48,7 +55,7 @@ const initializeI18nForDetector = async (
|
|
|
48
55
|
|
|
49
56
|
const safeUserOptions = pickSafeDetectionOptions(options.userInitOptions);
|
|
50
57
|
|
|
51
|
-
// Only initialize detection capability, don't load
|
|
58
|
+
// Only initialize detection capability, don't load resources to avoid conflicts with subsequent backend initialization
|
|
52
59
|
const initOptions: I18nInitOptions = {
|
|
53
60
|
...safeUserOptions,
|
|
54
61
|
fallbackLng: options.fallbackLanguage,
|
|
@@ -81,7 +88,9 @@ const initializeI18nForDetector = async (
|
|
|
81
88
|
*/
|
|
82
89
|
export const detectLanguageFromI18nextDetector = async (
|
|
83
90
|
i18nInstance: I18nInstance,
|
|
84
|
-
options: BaseLanguageDetectionOptions & {
|
|
91
|
+
options: BaseLanguageDetectionOptions & {
|
|
92
|
+
ssrContext?: LanguageDetectionSsrContext;
|
|
93
|
+
},
|
|
85
94
|
): Promise<string | undefined> => {
|
|
86
95
|
if (!options.i18nextDetector) {
|
|
87
96
|
return undefined;
|
|
@@ -108,7 +117,7 @@ export const detectLanguageFromI18nextDetector = async (
|
|
|
108
117
|
|
|
109
118
|
const detectorLang = detectLanguage(
|
|
110
119
|
detectorInstance,
|
|
111
|
-
request
|
|
120
|
+
request,
|
|
112
121
|
mergedDetection,
|
|
113
122
|
);
|
|
114
123
|
|
|
@@ -148,8 +157,8 @@ export const detectLanguageFromI18nextDetector = async (
|
|
|
148
157
|
});
|
|
149
158
|
} else if (detectorInstance === i18nInstance) {
|
|
150
159
|
// As a fallback, prevent i18nInstance from being polluted by detector init
|
|
151
|
-
|
|
152
|
-
delete (i18nInstance as
|
|
160
|
+
i18nInstance.isInitialized = false;
|
|
161
|
+
delete (i18nInstance as unknown as MutableDetectorInstanceState).language;
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
164
|
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { LanguageDetector } from 'i18next-http-middleware';
|
|
2
|
-
import type { I18nInstance } from '../instance';
|
|
2
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance';
|
|
3
|
+
|
|
4
|
+
type HttpDetectorInit = (
|
|
5
|
+
services: NonNullable<I18nInstance['services']>,
|
|
6
|
+
options?: unknown,
|
|
7
|
+
allOptions?: unknown,
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
type HttpDetectorDetect = (
|
|
11
|
+
request: unknown,
|
|
12
|
+
response: unknown,
|
|
13
|
+
detectionOrder?: unknown,
|
|
14
|
+
) => string | string[] | undefined;
|
|
3
15
|
|
|
4
16
|
export const cacheUserLanguage = (
|
|
5
17
|
_i18nInstance: I18nInstance,
|
|
6
18
|
_language: string,
|
|
7
|
-
_detectionOptions?:
|
|
19
|
+
_detectionOptions?: unknown,
|
|
8
20
|
): void => {
|
|
9
21
|
return;
|
|
10
22
|
};
|
|
@@ -14,7 +26,7 @@ export const cacheUserLanguage = (
|
|
|
14
26
|
* Not available in Node.js environment, returns undefined
|
|
15
27
|
*/
|
|
16
28
|
export const readLanguageFromStorage = (
|
|
17
|
-
_detectionOptions?:
|
|
29
|
+
_detectionOptions?: LanguageDetectorOptions,
|
|
18
30
|
): string | undefined => {
|
|
19
31
|
// In Node.js environment, storage-based detection is not available
|
|
20
32
|
return undefined;
|
|
@@ -36,8 +48,8 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
|
|
|
36
48
|
*/
|
|
37
49
|
export const detectLanguage = (
|
|
38
50
|
i18nInstance: I18nInstance,
|
|
39
|
-
request?:
|
|
40
|
-
detectionOptions?:
|
|
51
|
+
request?: unknown,
|
|
52
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
41
53
|
): string | undefined => {
|
|
42
54
|
if (!request) {
|
|
43
55
|
return undefined;
|
|
@@ -65,9 +77,16 @@ export const detectLanguage = (
|
|
|
65
77
|
const optionsToUse = detectionOptions
|
|
66
78
|
? { ...i18nInstance.options, detection: detectionOptions }
|
|
67
79
|
: i18nInstance.options;
|
|
68
|
-
manualDetector.init
|
|
80
|
+
(manualDetector.init as unknown as HttpDetectorInit)(
|
|
81
|
+
i18nInstance.services,
|
|
82
|
+
optionsToUse,
|
|
83
|
+
);
|
|
69
84
|
|
|
70
|
-
const result = (manualDetector.detect as
|
|
85
|
+
const result = (manualDetector.detect as unknown as HttpDetectorDetect)(
|
|
86
|
+
request,
|
|
87
|
+
{},
|
|
88
|
+
undefined,
|
|
89
|
+
);
|
|
71
90
|
if (typeof result === 'string') {
|
|
72
91
|
return result;
|
|
73
92
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
3
|
-
import type { I18nInstance } from '../instance';
|
|
3
|
+
import type { I18nInstance, LanguageDetectorOptions } from '../instance';
|
|
4
4
|
import { getActualI18nextInstance, isI18nWrapperInstance } from '../instance';
|
|
5
5
|
|
|
6
|
+
type BrowserDetectorInitOptions = Parameters<LanguageDetector['init']>[1];
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Register LanguageDetector plugin to i18n instance
|
|
8
10
|
* Must be called before init() to properly register the detector
|
|
@@ -27,7 +29,7 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
|
|
|
27
29
|
* Fallback when detector is not available in services
|
|
28
30
|
*/
|
|
29
31
|
export const readLanguageFromStorage = (
|
|
30
|
-
detectionOptions?:
|
|
32
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
31
33
|
): string | undefined => {
|
|
32
34
|
try {
|
|
33
35
|
const options = detectionOptions || {};
|
|
@@ -112,8 +114,8 @@ export const readLanguageFromStorage = (
|
|
|
112
114
|
*/
|
|
113
115
|
export const detectLanguage = (
|
|
114
116
|
i18nInstance: I18nInstance,
|
|
115
|
-
_request?:
|
|
116
|
-
detectionOptions?:
|
|
117
|
+
_request?: unknown,
|
|
118
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
117
119
|
): string | undefined => {
|
|
118
120
|
try {
|
|
119
121
|
// For wrapper instances, get the underlying i18next instance
|
|
@@ -160,7 +162,10 @@ export const detectLanguage = (
|
|
|
160
162
|
const mergedOptions = detectionOptions
|
|
161
163
|
? { ...optionsToUse, detection: detectionOptions }
|
|
162
164
|
: optionsToUse;
|
|
163
|
-
manualDetector.init(
|
|
165
|
+
manualDetector.init(
|
|
166
|
+
servicesToUse,
|
|
167
|
+
mergedOptions as unknown as BrowserDetectorInitOptions,
|
|
168
|
+
);
|
|
164
169
|
|
|
165
170
|
const result = manualDetector.detect();
|
|
166
171
|
if (typeof result === 'string') {
|
|
@@ -186,7 +191,7 @@ export const detectLanguage = (
|
|
|
186
191
|
export const cacheUserLanguage = (
|
|
187
192
|
i18nInstance: I18nInstance,
|
|
188
193
|
language: string,
|
|
189
|
-
detectionOptions?:
|
|
194
|
+
detectionOptions?: unknown,
|
|
190
195
|
): void => {
|
|
191
196
|
if (typeof window === 'undefined') {
|
|
192
197
|
return;
|
|
@@ -231,7 +236,10 @@ export const cacheUserLanguage = (
|
|
|
231
236
|
: optionsToUse;
|
|
232
237
|
|
|
233
238
|
const manualDetector = new LanguageDetector();
|
|
234
|
-
manualDetector.init(
|
|
239
|
+
manualDetector.init(
|
|
240
|
+
servicesToUse,
|
|
241
|
+
mergedOptions as unknown as BrowserDetectorInitOptions,
|
|
242
|
+
);
|
|
235
243
|
|
|
236
244
|
if (typeof manualDetector.cacheUserLanguage === 'function') {
|
|
237
245
|
manualDetector.cacheUserLanguage(language);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BackendOptions,
|
|
3
|
+
I18nInitOptions,
|
|
4
|
+
LanguageDetectorOptions,
|
|
5
|
+
} from '../instance';
|
|
6
|
+
|
|
7
|
+
export interface LanguageDetectionSsrContext {
|
|
8
|
+
request?: unknown;
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
export interface BaseLanguageDetectionOptions {
|
|
4
12
|
languages: string[];
|
|
@@ -7,12 +15,12 @@ export interface BaseLanguageDetectionOptions {
|
|
|
7
15
|
i18nextDetector: boolean;
|
|
8
16
|
detection?: LanguageDetectorOptions;
|
|
9
17
|
userInitOptions?: I18nInitOptions;
|
|
10
|
-
mergedBackend?:
|
|
18
|
+
mergedBackend?: BackendOptions;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export interface LanguageDetectionOptions extends BaseLanguageDetectionOptions {
|
|
14
22
|
pathname: string;
|
|
15
|
-
ssrContext?:
|
|
23
|
+
ssrContext?: LanguageDetectionSsrContext;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export interface LanguageDetectionResult {
|
|
@@ -28,6 +36,6 @@ export interface BuildInitOptionsParams {
|
|
|
28
36
|
fallbackLanguage: string;
|
|
29
37
|
languages: string[];
|
|
30
38
|
userInitOptions?: I18nInitOptions;
|
|
31
|
-
mergedDetection?:
|
|
32
|
-
mergeBackend?:
|
|
39
|
+
mergedDetection?: LanguageDetectorOptions;
|
|
40
|
+
mergeBackend?: BackendOptions;
|
|
33
41
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { BaseBackendOptions } from '../../shared/type';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface I18nResourceStore {
|
|
4
4
|
data?: {
|
|
5
5
|
[language: string]: {
|
|
6
|
-
[namespace: string]:
|
|
6
|
+
[namespace: string]: ResourceValue;
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
9
|
addResourceBundle?: (
|
|
@@ -15,30 +15,49 @@ export interface I18nResourceStore {
|
|
|
15
15
|
) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type I18nWrapperInstance = I18nInstance & {
|
|
19
|
+
i18nInstance: {
|
|
20
|
+
instance: I18nInstance;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function isI18nWrapperInstance(
|
|
25
|
+
obj: unknown,
|
|
26
|
+
): obj is I18nWrapperInstance {
|
|
19
27
|
if (!obj || typeof obj !== 'object') {
|
|
20
28
|
return false;
|
|
21
29
|
}
|
|
22
|
-
|
|
30
|
+
const candidate = obj as {
|
|
31
|
+
i18nInstance?: unknown;
|
|
32
|
+
init?: unknown;
|
|
33
|
+
use?: unknown;
|
|
34
|
+
};
|
|
35
|
+
if (!candidate.i18nInstance || typeof candidate.i18nInstance !== 'object') {
|
|
23
36
|
return false;
|
|
24
37
|
}
|
|
25
|
-
|
|
38
|
+
const wrapper = candidate.i18nInstance as { instance?: unknown };
|
|
39
|
+
if (!wrapper.instance) {
|
|
26
40
|
return false;
|
|
27
41
|
}
|
|
28
|
-
if (
|
|
42
|
+
if (
|
|
43
|
+
typeof candidate.init !== 'function' ||
|
|
44
|
+
typeof candidate.use !== 'function'
|
|
45
|
+
) {
|
|
29
46
|
return false;
|
|
30
47
|
}
|
|
31
48
|
return true;
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
function getI18nWrapperI18nextInstance(
|
|
52
|
+
wrapperInstance: unknown,
|
|
53
|
+
): I18nInstance | null {
|
|
35
54
|
if (isI18nWrapperInstance(wrapperInstance)) {
|
|
36
55
|
return wrapperInstance.i18nInstance?.instance;
|
|
37
56
|
}
|
|
38
57
|
return null;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
|
-
export function getActualI18nextInstance(instance: I18nInstance
|
|
60
|
+
export function getActualI18nextInstance(instance: I18nInstance): I18nInstance {
|
|
42
61
|
if (isI18nWrapperInstance(instance)) {
|
|
43
62
|
const i18nextInstance = getI18nWrapperI18nextInstance(instance);
|
|
44
63
|
return i18nextInstance || instance;
|
|
@@ -50,39 +69,42 @@ export interface I18nInstance {
|
|
|
50
69
|
language: string;
|
|
51
70
|
isInitialized?: boolean;
|
|
52
71
|
init: {
|
|
53
|
-
(callback?: (error:
|
|
72
|
+
(callback?: (error: unknown, t: unknown) => void): Promise<unknown>;
|
|
54
73
|
(
|
|
55
74
|
options: I18nInitOptions,
|
|
56
|
-
callback?: (error:
|
|
57
|
-
): Promise<
|
|
75
|
+
callback?: (error: unknown, t: unknown) => void,
|
|
76
|
+
): Promise<unknown>;
|
|
58
77
|
};
|
|
59
78
|
changeLanguage?: (
|
|
60
79
|
lng?: string,
|
|
61
|
-
callback?: (error:
|
|
62
|
-
) => Promise<
|
|
80
|
+
callback?: (error: unknown, t: unknown) => void,
|
|
81
|
+
) => Promise<unknown>;
|
|
63
82
|
setLang?: (lang: string) => void | Promise<void>;
|
|
64
|
-
use: (plugin:
|
|
83
|
+
use: (plugin: unknown) => void;
|
|
65
84
|
createInstance?: (options?: I18nInitOptions) => I18nInstance;
|
|
66
85
|
cloneInstance?: () => I18nInstance; // ssr need
|
|
67
86
|
// i18next store (may not be in type definition but exists at runtime)
|
|
68
87
|
store?: I18nResourceStore;
|
|
69
|
-
emit?: (event: string, ...args:
|
|
88
|
+
emit?: (event: string, ...args: unknown[]) => void;
|
|
70
89
|
reloadResources?: (language?: string, namespace?: string) => Promise<void>;
|
|
71
90
|
services?: {
|
|
72
91
|
languageDetector?: {
|
|
73
|
-
detect: (
|
|
74
|
-
|
|
92
|
+
detect: (
|
|
93
|
+
request?: unknown,
|
|
94
|
+
options?: unknown,
|
|
95
|
+
) => string | string[] | undefined;
|
|
96
|
+
[key: string]: unknown;
|
|
75
97
|
};
|
|
76
98
|
resourceStore?: I18nResourceStore;
|
|
77
|
-
backend?:
|
|
78
|
-
[key: string]:
|
|
99
|
+
backend?: unknown; // Backend instance (e.g., SdkBackend)
|
|
100
|
+
[key: string]: unknown;
|
|
79
101
|
};
|
|
80
102
|
// i18next instance options (available after initialization)
|
|
81
103
|
options?: {
|
|
82
104
|
backend?: BackendOptions;
|
|
83
|
-
[key: string]:
|
|
105
|
+
[key: string]: unknown;
|
|
84
106
|
};
|
|
85
|
-
[key: string]:
|
|
107
|
+
[key: string]: unknown;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
type LanguageDetectorOrder = string[];
|
|
@@ -101,12 +123,12 @@ export interface LanguageDetectorOptions {
|
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
export interface BackendOptions extends Omit<BaseBackendOptions, 'enabled'> {
|
|
104
|
-
parse?: (data: string) =>
|
|
105
|
-
stringify?: (data:
|
|
126
|
+
parse?: (data: string) => unknown;
|
|
127
|
+
stringify?: (data: unknown) => string;
|
|
106
128
|
[key: string]: any;
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
type ResourceValue = string | { [key: string]: ResourceValue };
|
|
110
132
|
|
|
111
133
|
export interface Resources {
|
|
112
134
|
[lng: string]: {
|
|
@@ -126,15 +148,15 @@ export type I18nInitOptions = {
|
|
|
126
148
|
defaultNS?: string | string[];
|
|
127
149
|
interpolation?: {
|
|
128
150
|
escapeValue?: boolean;
|
|
129
|
-
[key: string]:
|
|
151
|
+
[key: string]: unknown;
|
|
130
152
|
};
|
|
131
153
|
react?: {
|
|
132
154
|
useSuspense?: boolean;
|
|
133
|
-
[key: string]:
|
|
155
|
+
[key: string]: unknown;
|
|
134
156
|
};
|
|
135
157
|
};
|
|
136
158
|
|
|
137
|
-
export function isI18nInstance(obj:
|
|
159
|
+
export function isI18nInstance(obj: unknown): obj is I18nInstance {
|
|
138
160
|
if (!obj || typeof obj !== 'object') {
|
|
139
161
|
return false;
|
|
140
162
|
}
|
|
@@ -143,7 +165,10 @@ export function isI18nInstance(obj: any): obj is I18nInstance {
|
|
|
143
165
|
return true;
|
|
144
166
|
}
|
|
145
167
|
|
|
146
|
-
|
|
168
|
+
const candidate = obj as { init?: unknown; use?: unknown };
|
|
169
|
+
return (
|
|
170
|
+
typeof candidate.init === 'function' && typeof candidate.use === 'function'
|
|
171
|
+
);
|
|
147
172
|
}
|
|
148
173
|
|
|
149
174
|
async function tryImportI18next(): Promise<I18nInstance | null> {
|
|
@@ -170,8 +195,8 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
|
|
|
170
195
|
}
|
|
171
196
|
|
|
172
197
|
export function getI18nextInstanceForProvider(
|
|
173
|
-
instance: I18nInstance
|
|
174
|
-
):
|
|
198
|
+
instance: I18nInstance,
|
|
199
|
+
): I18nInstance {
|
|
175
200
|
if (isI18nWrapperInstance(instance)) {
|
|
176
201
|
const i18nextInstance = getI18nWrapperI18nextInstance(instance);
|
|
177
202
|
if (i18nextInstance) {
|
|
@@ -183,7 +208,7 @@ export function getI18nextInstanceForProvider(
|
|
|
183
208
|
}
|
|
184
209
|
|
|
185
210
|
export async function getI18nInstance(
|
|
186
|
-
userInstance?:
|
|
211
|
+
userInstance?: unknown,
|
|
187
212
|
): Promise<I18nInstance> {
|
|
188
213
|
if (userInstance) {
|
|
189
214
|
if (isI18nWrapperInstance(userInstance)) {
|
|
@@ -11,7 +11,16 @@ import {
|
|
|
11
11
|
isI18nWrapperInstance,
|
|
12
12
|
} from './instance';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
type MergedBackendOptions = NonNullable<I18nInitOptions['backend']> & {
|
|
15
|
+
_useChainedBackend?: boolean;
|
|
16
|
+
_chainedBackendConfig?: {
|
|
17
|
+
backendOptions: Array<Record<string, unknown>>;
|
|
18
|
+
};
|
|
19
|
+
backends?: unknown[];
|
|
20
|
+
backendOptions?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function assertI18nInstance(obj: unknown): asserts obj is I18nInstance {
|
|
15
24
|
if (!isI18nInstance(obj)) {
|
|
16
25
|
throw new Error('Object does not implement I18nInstance interface');
|
|
17
26
|
}
|
|
@@ -24,8 +33,8 @@ export const buildInitOptions = async (
|
|
|
24
33
|
finalLanguage: string,
|
|
25
34
|
fallbackLanguage: string,
|
|
26
35
|
languages: string[],
|
|
27
|
-
mergedDetection:
|
|
28
|
-
mergedBackend:
|
|
36
|
+
mergedDetection: I18nInitOptions['detection'],
|
|
37
|
+
mergedBackend: MergedBackendOptions | undefined,
|
|
29
38
|
userInitOptions?: I18nInitOptions,
|
|
30
39
|
useSuspense?: boolean,
|
|
31
40
|
i18nInstance?: I18nInstance,
|
|
@@ -84,8 +93,8 @@ export const buildInitOptions = async (
|
|
|
84
93
|
if (isChainedBackend && mergedBackend._chainedBackendConfig) {
|
|
85
94
|
// Try to get backend classes from i18nInstance.options.backend.backends first
|
|
86
95
|
// This avoids importing fs-backend in browser environment
|
|
87
|
-
let HttpBackend:
|
|
88
|
-
let SdkBackendClass:
|
|
96
|
+
let HttpBackend: unknown;
|
|
97
|
+
let SdkBackendClass: unknown;
|
|
89
98
|
|
|
90
99
|
if (
|
|
91
100
|
i18nInstance?.options?.backend?.backends &&
|
|
@@ -148,7 +157,7 @@ export const changeI18nLanguage = async (
|
|
|
148
157
|
i18nInstance: I18nInstance,
|
|
149
158
|
newLang: string,
|
|
150
159
|
options?: {
|
|
151
|
-
detectionOptions?:
|
|
160
|
+
detectionOptions?: I18nInitOptions['detection'];
|
|
152
161
|
},
|
|
153
162
|
): Promise<void> => {
|
|
154
163
|
if (!newLang || typeof newLang !== 'string') {
|
|
@@ -179,8 +188,8 @@ export const initializeI18nInstance = async (
|
|
|
179
188
|
finalLanguage: string,
|
|
180
189
|
fallbackLanguage: string,
|
|
181
190
|
languages: string[],
|
|
182
|
-
mergedDetection:
|
|
183
|
-
mergedBackend:
|
|
191
|
+
mergedDetection: I18nInitOptions['detection'],
|
|
192
|
+
mergedBackend: MergedBackendOptions | undefined,
|
|
184
193
|
userInitOptions?: I18nInitOptions,
|
|
185
194
|
useSuspense?: boolean,
|
|
186
195
|
): Promise<void> => {
|
|
@@ -265,7 +274,7 @@ export const setupClonedInstance = async (
|
|
|
265
274
|
backendEnabled: boolean,
|
|
266
275
|
backend: BaseBackendOptions | undefined,
|
|
267
276
|
i18nextDetector: boolean,
|
|
268
|
-
detection:
|
|
277
|
+
detection: I18nInitOptions['detection'],
|
|
269
278
|
localePathRedirect: boolean,
|
|
270
279
|
userInitOptions: I18nInitOptions | undefined,
|
|
271
280
|
): Promise<void> => {
|
|
@@ -57,7 +57,7 @@ export interface RuntimeContextWithI18n extends TInternalRuntimeContext {
|
|
|
57
57
|
changeLanguage?: (lang: string) => Promise<void>;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
interface SetupI18nBeforeRenderOptions {
|
|
61
61
|
api: I18nRuntimeApi;
|
|
62
62
|
userI18nInstance?: I18nInstance;
|
|
63
63
|
initOptions?: I18nInitOptions;
|
|
@@ -14,7 +14,7 @@ import type { I18nInstance } from './i18n';
|
|
|
14
14
|
import { getI18nextInstanceForProvider } from './i18n/instance';
|
|
15
15
|
import type { RuntimeContextWithI18n } from './pluginSetup';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface I18nRootWrapperOptions {
|
|
18
18
|
entryName?: string;
|
|
19
19
|
htmlLangAttr: boolean;
|
|
20
20
|
localePathRedirect: boolean;
|
|
@@ -15,31 +15,31 @@ import {
|
|
|
15
15
|
import type React from 'react';
|
|
16
16
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface I18nRouterLocation {
|
|
21
21
|
pathname: string;
|
|
22
22
|
search: string;
|
|
23
23
|
hash: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface I18nRouterNavigateOptions {
|
|
27
27
|
replace?: boolean;
|
|
28
28
|
state?: unknown;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
type I18nRouterNavigate = (
|
|
32
32
|
href: string,
|
|
33
33
|
options?: I18nRouterNavigateOptions,
|
|
34
34
|
) => void | Promise<void>;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
type I18nRouterLink = React.ComponentType<{
|
|
37
37
|
to: string;
|
|
38
38
|
children?: React.ReactNode;
|
|
39
39
|
[key: string]: unknown;
|
|
40
40
|
}>;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
interface I18nRouterAdapter {
|
|
43
43
|
framework?: I18nRouterFramework;
|
|
44
44
|
hasRouter: boolean;
|
|
45
45
|
location: I18nRouterLocation | null;
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import { getGlobalBasename } from '@modern-js/runtime/context';
|
|
3
3
|
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
localiseTargetPathname,
|
|
6
|
+
shouldSkipLocaleRedirect,
|
|
7
|
+
} from '../shared/localisedUrls';
|
|
5
8
|
|
|
6
9
|
// Structural parameter: hooks.ts passes a public-TRuntimeContext-based
|
|
7
10
|
// context while core.tsx passes the internal one; both carry the request
|
|
@@ -129,32 +132,5 @@ export const shouldIgnoreRedirect = (
|
|
|
129
132
|
languages: string[],
|
|
130
133
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
131
134
|
): boolean => {
|
|
132
|
-
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Remove language prefix if present (e.g., /en/api -> /api)
|
|
137
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
138
|
-
let pathWithoutLang = pathname;
|
|
139
|
-
if (segments.length > 0 && languages.includes(segments[0])) {
|
|
140
|
-
// Remove language prefix
|
|
141
|
-
pathWithoutLang = `/${segments.slice(1).join('/')}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Normalize path (ensure it starts with /)
|
|
145
|
-
const normalizedPath = pathWithoutLang.startsWith('/')
|
|
146
|
-
? pathWithoutLang
|
|
147
|
-
: `/${pathWithoutLang}`;
|
|
148
|
-
|
|
149
|
-
if (typeof ignoreRedirectRoutes === 'function') {
|
|
150
|
-
return ignoreRedirectRoutes(normalizedPath);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Check if pathname matches any of the ignore patterns
|
|
154
|
-
return ignoreRedirectRoutes.some(pattern => {
|
|
155
|
-
// Support both exact match and prefix match
|
|
156
|
-
return (
|
|
157
|
-
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
|
|
158
|
-
);
|
|
159
|
-
});
|
|
135
|
+
return shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
|
|
160
136
|
};
|