@modern-js/plugin-i18n 2.69.7 → 3.0.0-alpha.1
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/README.md +6 -0
- package/dist/cjs/cli/index.cjs +154 -0
- package/dist/cjs/runtime/I18nLink.cjs +68 -0
- package/dist/cjs/runtime/context.cjs +138 -0
- package/dist/cjs/runtime/hooks.cjs +189 -0
- package/dist/cjs/runtime/i18n/backend/config.cjs +39 -0
- package/dist/cjs/runtime/i18n/backend/defaults.cjs +56 -0
- package/dist/cjs/runtime/i18n/backend/defaults.node.cjs +56 -0
- package/dist/cjs/runtime/i18n/backend/index.cjs +108 -0
- package/dist/cjs/runtime/i18n/backend/middleware.cjs +54 -0
- package/dist/cjs/runtime/i18n/backend/middleware.common.cjs +105 -0
- package/dist/cjs/runtime/i18n/backend/middleware.node.cjs +58 -0
- package/dist/cjs/runtime/i18n/backend/sdk-backend.cjs +171 -0
- package/dist/cjs/runtime/i18n/detection/config.cjs +63 -0
- package/dist/cjs/runtime/i18n/detection/index.cjs +309 -0
- package/dist/cjs/runtime/i18n/detection/middleware.cjs +185 -0
- package/dist/cjs/runtime/i18n/detection/middleware.node.cjs +74 -0
- package/dist/cjs/runtime/i18n/index.cjs +43 -0
- package/dist/cjs/runtime/i18n/instance.cjs +132 -0
- package/dist/cjs/runtime/i18n/utils.cjs +185 -0
- package/dist/cjs/runtime/index.cjs +172 -0
- package/dist/cjs/runtime/types.cjs +18 -0
- package/dist/cjs/runtime/utils.cjs +134 -0
- package/dist/cjs/server/index.cjs +178 -0
- package/dist/cjs/shared/deepMerge.cjs +54 -0
- package/dist/cjs/shared/detection.cjs +105 -0
- package/dist/cjs/shared/type.cjs +18 -0
- package/dist/cjs/shared/utils.cjs +78 -0
- package/dist/esm/cli/index.js +106 -0
- package/dist/esm/runtime/I18nLink.js +31 -0
- package/dist/esm/runtime/context.js +101 -0
- package/dist/esm/runtime/hooks.js +146 -0
- package/dist/esm/runtime/i18n/backend/config.js +5 -0
- package/dist/esm/runtime/i18n/backend/defaults.js +19 -0
- package/dist/esm/runtime/i18n/backend/defaults.node.js +19 -0
- package/dist/esm/runtime/i18n/backend/index.js +74 -0
- package/dist/esm/runtime/i18n/backend/middleware.common.js +61 -0
- package/dist/esm/runtime/i18n/backend/middleware.js +7 -0
- package/dist/esm/runtime/i18n/backend/middleware.node.js +8 -0
- package/dist/esm/runtime/i18n/backend/sdk-backend.js +137 -0
- package/dist/esm/runtime/i18n/detection/config.js +26 -0
- package/dist/esm/runtime/i18n/detection/index.js +260 -0
- package/dist/esm/runtime/i18n/detection/middleware.js +132 -0
- package/dist/esm/runtime/i18n/detection/middleware.node.js +31 -0
- package/dist/esm/runtime/i18n/index.js +3 -0
- package/dist/esm/runtime/i18n/instance.js +77 -0
- package/dist/esm/runtime/i18n/utils.js +136 -0
- package/dist/esm/runtime/index.js +129 -0
- package/dist/esm/runtime/types.js +0 -0
- package/dist/esm/runtime/utils.js +82 -0
- package/dist/esm/server/index.js +168 -0
- package/dist/esm/shared/deepMerge.js +20 -0
- package/dist/esm/shared/detection.js +71 -0
- package/dist/esm/shared/type.js +0 -0
- package/dist/esm/shared/utils.js +35 -0
- package/dist/esm-node/cli/index.js +106 -0
- package/dist/esm-node/runtime/I18nLink.js +31 -0
- package/dist/esm-node/runtime/context.js +101 -0
- package/dist/esm-node/runtime/hooks.js +146 -0
- package/dist/esm-node/runtime/i18n/backend/config.js +5 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.js +19 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.node.js +19 -0
- package/dist/esm-node/runtime/i18n/backend/index.js +74 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.common.js +61 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.js +7 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.node.js +8 -0
- package/dist/esm-node/runtime/i18n/backend/sdk-backend.js +137 -0
- package/dist/esm-node/runtime/i18n/detection/config.js +26 -0
- package/dist/esm-node/runtime/i18n/detection/index.js +260 -0
- package/dist/esm-node/runtime/i18n/detection/middleware.js +132 -0
- package/dist/esm-node/runtime/i18n/detection/middleware.node.js +31 -0
- package/dist/esm-node/runtime/i18n/index.js +3 -0
- package/dist/esm-node/runtime/i18n/instance.js +77 -0
- package/dist/esm-node/runtime/i18n/utils.js +136 -0
- package/dist/esm-node/runtime/index.js +129 -0
- package/dist/esm-node/runtime/types.js +0 -0
- package/dist/esm-node/runtime/utils.js +82 -0
- package/dist/esm-node/server/index.js +168 -0
- package/dist/esm-node/shared/deepMerge.js +20 -0
- package/dist/esm-node/shared/detection.js +71 -0
- package/dist/esm-node/shared/type.js +0 -0
- package/dist/esm-node/shared/utils.js +35 -0
- package/dist/types/cli/index.d.ts +21 -0
- package/dist/types/runtime/I18nLink.d.ts +8 -0
- package/dist/types/runtime/context.d.ts +38 -0
- package/dist/types/runtime/hooks.d.ts +28 -0
- package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
- package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
- package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +52 -0
- package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
- package/dist/types/runtime/i18n/index.d.ts +3 -0
- package/dist/types/runtime/i18n/instance.d.ts +93 -0
- package/dist/types/runtime/i18n/utils.d.ts +29 -0
- package/dist/types/runtime/index.d.ts +20 -0
- package/dist/types/runtime/types.d.ts +15 -0
- package/dist/types/runtime/utils.d.ts +33 -0
- package/dist/types/server/index.d.ts +8 -0
- package/dist/types/shared/deepMerge.d.ts +1 -0
- package/dist/types/shared/detection.d.ts +11 -0
- package/dist/types/shared/type.d.ts +156 -0
- package/dist/types/shared/utils.d.ts +5 -0
- package/package.json +100 -34
- package/rslib.config.mts +4 -0
- package/src/cli/index.ts +245 -0
- package/src/runtime/I18nLink.tsx +76 -0
- package/src/runtime/context.tsx +256 -0
- package/src/runtime/hooks.ts +274 -0
- package/src/runtime/i18n/backend/config.ts +10 -0
- package/src/runtime/i18n/backend/defaults.node.ts +31 -0
- package/src/runtime/i18n/backend/defaults.ts +37 -0
- package/src/runtime/i18n/backend/index.ts +181 -0
- package/src/runtime/i18n/backend/middleware.common.ts +116 -0
- package/src/runtime/i18n/backend/middleware.node.ts +32 -0
- package/src/runtime/i18n/backend/middleware.ts +28 -0
- package/src/runtime/i18n/backend/sdk-backend.ts +292 -0
- package/src/runtime/i18n/detection/config.ts +32 -0
- package/src/runtime/i18n/detection/index.ts +641 -0
- package/src/runtime/i18n/detection/middleware.node.ts +84 -0
- package/src/runtime/i18n/detection/middleware.ts +251 -0
- package/src/runtime/i18n/index.ts +8 -0
- package/src/runtime/i18n/instance.ts +227 -0
- package/src/runtime/i18n/utils.ts +333 -0
- package/src/runtime/index.tsx +281 -0
- package/src/runtime/types.ts +17 -0
- package/src/runtime/utils.ts +151 -0
- package/src/server/index.ts +336 -0
- package/src/shared/deepMerge.ts +38 -0
- package/src/shared/detection.ts +131 -0
- package/src/shared/type.ts +170 -0
- package/src/shared/utils.ts +82 -0
- package/tsconfig.json +12 -0
- package/dist/cjs/index.js +0 -73
- package/dist/cjs/languageDetector.js +0 -51
- package/dist/cjs/utils/index.js +0 -39
- package/dist/esm/index.js +0 -61
- package/dist/esm/languageDetector.js +0 -33
- package/dist/esm/utils/index.js +0 -16
- package/dist/esm-node/index.js +0 -49
- package/dist/esm-node/languageDetector.js +0 -26
- package/dist/esm-node/utils/index.js +0 -15
- package/dist/types/index.d.ts +0 -34
- package/dist/types/languageDetector.d.ts +0 -6
- package/dist/types/utils/index.d.ts +0 -5
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { type TRuntimeContext, isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import { detectLanguageFromPath } from '../../utils';
|
|
3
|
+
import type {
|
|
4
|
+
I18nInitOptions,
|
|
5
|
+
I18nInstance,
|
|
6
|
+
LanguageDetectorOptions,
|
|
7
|
+
} from '../instance';
|
|
8
|
+
import { isI18nWrapperInstance } from '../instance';
|
|
9
|
+
import { mergeDetectionOptions as mergeDetectionOptionsUtil } from './config';
|
|
10
|
+
import {
|
|
11
|
+
cacheUserLanguage,
|
|
12
|
+
detectLanguage,
|
|
13
|
+
readLanguageFromStorage,
|
|
14
|
+
useI18nextLanguageDetector,
|
|
15
|
+
} from './middleware';
|
|
16
|
+
|
|
17
|
+
// Re-export cacheUserLanguage for use in context
|
|
18
|
+
export { cacheUserLanguage };
|
|
19
|
+
|
|
20
|
+
interface DetectorCacheEntry {
|
|
21
|
+
instance: I18nInstance;
|
|
22
|
+
isTemporary: boolean;
|
|
23
|
+
configKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const detectorInstanceCache = new WeakMap<I18nInstance, DetectorCacheEntry>();
|
|
27
|
+
|
|
28
|
+
const DETECTOR_SAFE_OPTION_KEYS: string[] = [
|
|
29
|
+
'lowerCaseLng',
|
|
30
|
+
'nonExplicitSupportedLngs',
|
|
31
|
+
'load',
|
|
32
|
+
'partialBundledLanguages',
|
|
33
|
+
'returnNull',
|
|
34
|
+
'returnEmptyString',
|
|
35
|
+
'returnObjects',
|
|
36
|
+
'joinArrays',
|
|
37
|
+
'keySeparator',
|
|
38
|
+
'nsSeparator',
|
|
39
|
+
'pluralSeparator',
|
|
40
|
+
'contextSeparator',
|
|
41
|
+
'fallbackNS',
|
|
42
|
+
'ns',
|
|
43
|
+
'defaultNS',
|
|
44
|
+
'debug',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stable stringify that sorts object keys to ensure consistent output
|
|
49
|
+
* regardless of property order
|
|
50
|
+
*/
|
|
51
|
+
const stableStringify = (value: any): string => {
|
|
52
|
+
if (value === null || value === undefined) {
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value !== 'object') {
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
// Arrays maintain their order
|
|
62
|
+
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// For objects, sort keys and recursively stringify values
|
|
66
|
+
const sortedKeys = Object.keys(value).sort();
|
|
67
|
+
const sortedEntries = sortedKeys.map(key => {
|
|
68
|
+
const stringifiedValue = stableStringify(value[key]);
|
|
69
|
+
return `${JSON.stringify(key)}:${stringifiedValue}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return `{${sortedEntries.join(',')}}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const buildDetectorConfigKey = (
|
|
76
|
+
languages: string[],
|
|
77
|
+
fallbackLanguage: string,
|
|
78
|
+
mergedDetection: LanguageDetectorOptions,
|
|
79
|
+
): string => {
|
|
80
|
+
return stableStringify({
|
|
81
|
+
languages,
|
|
82
|
+
fallbackLanguage,
|
|
83
|
+
detection: mergedDetection,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const pickSafeDetectionOptions = (
|
|
88
|
+
userInitOptions?: I18nInitOptions,
|
|
89
|
+
): Partial<I18nInitOptions> & Record<string, any> => {
|
|
90
|
+
if (!userInitOptions) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
const safeOptions: Partial<I18nInitOptions> & Record<string, any> = {};
|
|
94
|
+
for (const key of DETECTOR_SAFE_OPTION_KEYS) {
|
|
95
|
+
const value = (userInitOptions as any)[key];
|
|
96
|
+
if (value !== undefined) {
|
|
97
|
+
safeOptions[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if ((userInitOptions as any).interpolation) {
|
|
101
|
+
safeOptions.interpolation = { ...(userInitOptions as any).interpolation };
|
|
102
|
+
}
|
|
103
|
+
return safeOptions;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const cleanupDetectorCacheEntry = (entry?: DetectorCacheEntry) => {
|
|
107
|
+
if (!entry || !entry.isTemporary) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const instance = entry.instance as any;
|
|
111
|
+
try {
|
|
112
|
+
instance?.removeAllListeners?.();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
void error;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
instance?.off?.('*');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
void error;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
instance?.services?.backendConnector?.backend?.stop?.();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
void error;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
instance?.services?.backendConnector?.backend?.close?.();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
void error;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function exportServerLngToWindow(context: TRuntimeContext, lng: string) {
|
|
134
|
+
context.__i18nData__ = { lng };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const getLanguageFromSSRData = (window: Window): string | undefined => {
|
|
138
|
+
try {
|
|
139
|
+
const ssrData = window._SSR_DATA;
|
|
140
|
+
// Check if SSR data exists and has valid structure
|
|
141
|
+
if (!ssrData || !ssrData.data || !ssrData.data.i18nData) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const lng = ssrData.data.i18nData.lng;
|
|
145
|
+
// Return language only if it's a non-empty string
|
|
146
|
+
return typeof lng === 'string' && lng.trim() !== '' ? lng : undefined;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// If accessing window._SSR_DATA throws an error, return undefined
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export interface BaseLanguageDetectionOptions {
|
|
154
|
+
languages: string[];
|
|
155
|
+
fallbackLanguage: string;
|
|
156
|
+
localePathRedirect: boolean;
|
|
157
|
+
i18nextDetector: boolean;
|
|
158
|
+
detection?: LanguageDetectorOptions;
|
|
159
|
+
userInitOptions?: I18nInitOptions;
|
|
160
|
+
mergedBackend?: any;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface LanguageDetectionOptions extends BaseLanguageDetectionOptions {
|
|
164
|
+
pathname: string;
|
|
165
|
+
ssrContext?: any;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface LanguageDetectionResult {
|
|
169
|
+
detectedLanguage?: string;
|
|
170
|
+
finalLanguage: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Normalize language code (e.g., 'zh-CN' -> 'zh', 'en-US' -> 'en')
|
|
175
|
+
*/
|
|
176
|
+
const normalizeLanguageCode = (language: string): string => {
|
|
177
|
+
if (!language) {
|
|
178
|
+
return language;
|
|
179
|
+
}
|
|
180
|
+
// Extract base language code (before hyphen)
|
|
181
|
+
const baseLang = language.split('-')[0];
|
|
182
|
+
return baseLang;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if a language is supported
|
|
187
|
+
* Also checks the base language code (e.g., 'zh-CN' matches 'zh')
|
|
188
|
+
*/
|
|
189
|
+
const isLanguageSupported = (
|
|
190
|
+
language: string | undefined,
|
|
191
|
+
supportedLanguages: string[],
|
|
192
|
+
): boolean => {
|
|
193
|
+
if (!language) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (supportedLanguages.length === 0) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
// Check exact match first
|
|
200
|
+
if (supportedLanguages.includes(language)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
// Check base language code match (e.g., 'zh-CN' matches 'zh')
|
|
204
|
+
const baseLang = normalizeLanguageCode(language);
|
|
205
|
+
if (baseLang !== language && supportedLanguages.includes(baseLang)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the supported language that matches the given language
|
|
213
|
+
* Returns the exact match if available, otherwise returns the base language code match
|
|
214
|
+
* Returns undefined if no match is found
|
|
215
|
+
*/
|
|
216
|
+
const getSupportedLanguage = (
|
|
217
|
+
language: string | undefined,
|
|
218
|
+
supportedLanguages: string[],
|
|
219
|
+
): string | undefined => {
|
|
220
|
+
if (!language) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
if (supportedLanguages.length === 0) {
|
|
224
|
+
return language;
|
|
225
|
+
}
|
|
226
|
+
// Check exact match first
|
|
227
|
+
if (supportedLanguages.includes(language)) {
|
|
228
|
+
return language;
|
|
229
|
+
}
|
|
230
|
+
// Check base language code match (e.g., 'zh-CN' matches 'zh')
|
|
231
|
+
const baseLang = normalizeLanguageCode(language);
|
|
232
|
+
if (baseLang !== language && supportedLanguages.includes(baseLang)) {
|
|
233
|
+
return baseLang;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Priority 1: Detect language from SSR data
|
|
240
|
+
* Try to get language from window._SSR_DATA first (both SSR and CSR projects)
|
|
241
|
+
* Returns undefined if SSR data is not available or invalid
|
|
242
|
+
*/
|
|
243
|
+
const detectLanguageFromSSR = (languages: string[]): string | undefined => {
|
|
244
|
+
if (!isBrowser()) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const ssrLanguage = getLanguageFromSSRData(window);
|
|
250
|
+
if (ssrLanguage && isLanguageSupported(ssrLanguage, languages)) {
|
|
251
|
+
return ssrLanguage;
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Silently ignore errors
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return undefined;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Priority 2: Detect language from URL path
|
|
262
|
+
* Only returns a language if the path explicitly contains a language prefix
|
|
263
|
+
*/
|
|
264
|
+
const detectLanguageFromPathPriority = (
|
|
265
|
+
pathname: string,
|
|
266
|
+
languages: string[],
|
|
267
|
+
localePathRedirect: boolean,
|
|
268
|
+
): string | undefined => {
|
|
269
|
+
if (!localePathRedirect) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// If no languages are configured, cannot detect from path
|
|
274
|
+
if (!languages || languages.length === 0) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If pathname is empty or invalid, no language in path
|
|
279
|
+
if (!pathname || pathname.trim() === '') {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const pathDetection = detectLanguageFromPath(
|
|
285
|
+
pathname,
|
|
286
|
+
languages,
|
|
287
|
+
localePathRedirect,
|
|
288
|
+
);
|
|
289
|
+
// Only return language if explicitly detected in path
|
|
290
|
+
if (pathDetection.detected === true && pathDetection.language) {
|
|
291
|
+
return pathDetection.language;
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// Silently ignore errors, return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return undefined;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Initialize i18n instance for detector if needed
|
|
302
|
+
*/
|
|
303
|
+
interface DetectorInitResult {
|
|
304
|
+
detectorInstance: I18nInstance;
|
|
305
|
+
isTemporary: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const createDetectorInstance = (
|
|
309
|
+
baseInstance: I18nInstance,
|
|
310
|
+
configKey: string,
|
|
311
|
+
): { instance: I18nInstance; isTemporary: boolean } => {
|
|
312
|
+
const cached = detectorInstanceCache.get(baseInstance);
|
|
313
|
+
if (cached && cached.configKey === configKey) {
|
|
314
|
+
return { instance: cached.instance, isTemporary: cached.isTemporary };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cached) {
|
|
318
|
+
cleanupDetectorCacheEntry(cached);
|
|
319
|
+
detectorInstanceCache.delete(baseInstance);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const createNewInstance = (): {
|
|
323
|
+
instance: I18nInstance;
|
|
324
|
+
isTemporary: boolean;
|
|
325
|
+
} => {
|
|
326
|
+
if (typeof baseInstance.createInstance === 'function') {
|
|
327
|
+
try {
|
|
328
|
+
const created = baseInstance.createInstance();
|
|
329
|
+
if (created) {
|
|
330
|
+
return { instance: created, isTemporary: true };
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
void error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (typeof baseInstance.cloneInstance === 'function') {
|
|
338
|
+
try {
|
|
339
|
+
const cloned = baseInstance.cloneInstance();
|
|
340
|
+
if (cloned) {
|
|
341
|
+
return { instance: cloned, isTemporary: true };
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
void error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { instance: baseInstance, isTemporary: false };
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const created = createNewInstance();
|
|
352
|
+
if (created.isTemporary) {
|
|
353
|
+
detectorInstanceCache.set(baseInstance, {
|
|
354
|
+
instance: created.instance,
|
|
355
|
+
isTemporary: true,
|
|
356
|
+
configKey,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return created;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const initializeI18nForDetector = async (
|
|
363
|
+
i18nInstance: I18nInstance,
|
|
364
|
+
options: BaseLanguageDetectionOptions,
|
|
365
|
+
): Promise<DetectorInitResult> => {
|
|
366
|
+
const mergedDetection = mergeDetectionOptions(
|
|
367
|
+
options.i18nextDetector,
|
|
368
|
+
options.detection,
|
|
369
|
+
options.localePathRedirect,
|
|
370
|
+
options.userInitOptions,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const configKey = buildDetectorConfigKey(
|
|
374
|
+
options.languages,
|
|
375
|
+
options.fallbackLanguage,
|
|
376
|
+
mergedDetection,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const { instance, isTemporary } = createDetectorInstance(
|
|
380
|
+
i18nInstance,
|
|
381
|
+
configKey,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const safeUserOptions = pickSafeDetectionOptions(options.userInitOptions);
|
|
385
|
+
|
|
386
|
+
// Only initialize detection capability, don't load any resources to avoid conflicts with subsequent backend initialization
|
|
387
|
+
const initOptions: I18nInitOptions = {
|
|
388
|
+
...safeUserOptions,
|
|
389
|
+
fallbackLng: options.fallbackLanguage,
|
|
390
|
+
supportedLngs: options.languages,
|
|
391
|
+
detection: mergedDetection,
|
|
392
|
+
initImmediate: true,
|
|
393
|
+
interpolation: {
|
|
394
|
+
...(safeUserOptions?.interpolation || {}),
|
|
395
|
+
escapeValue: safeUserOptions?.interpolation?.escapeValue ?? false,
|
|
396
|
+
},
|
|
397
|
+
react: {
|
|
398
|
+
useSuspense: false,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Ensure the detector instance has the language detection plugin loaded
|
|
403
|
+
useI18nextLanguageDetector(instance);
|
|
404
|
+
|
|
405
|
+
if (!instance.isInitialized) {
|
|
406
|
+
await instance.init(initOptions);
|
|
407
|
+
} else if (isTemporary) {
|
|
408
|
+
await instance.init(initOptions);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { detectorInstance: instance, isTemporary };
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Priority 3: Detect language using i18next detector
|
|
416
|
+
*/
|
|
417
|
+
const detectLanguageFromI18nextDetector = async (
|
|
418
|
+
i18nInstance: I18nInstance,
|
|
419
|
+
options: BaseLanguageDetectionOptions & { ssrContext?: any },
|
|
420
|
+
): Promise<string | undefined> => {
|
|
421
|
+
if (!options.i18nextDetector) {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Merge detection options to pass to detector
|
|
426
|
+
const mergedDetection = mergeDetectionOptions(
|
|
427
|
+
options.i18nextDetector,
|
|
428
|
+
options.detection,
|
|
429
|
+
options.localePathRedirect,
|
|
430
|
+
options.userInitOptions,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const { detectorInstance, isTemporary } = await initializeI18nForDetector(
|
|
434
|
+
i18nInstance,
|
|
435
|
+
options,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const request = options.ssrContext?.request;
|
|
440
|
+
if (!isBrowser() && !request) {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const detectorLang = detectLanguage(
|
|
445
|
+
detectorInstance,
|
|
446
|
+
request as any,
|
|
447
|
+
mergedDetection,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Use getSupportedLanguage to get the matching supported language
|
|
451
|
+
// This handles both exact match and base language code match (e.g., 'zh-CN' -> 'zh')
|
|
452
|
+
if (detectorLang) {
|
|
453
|
+
const supportedLang = getSupportedLanguage(
|
|
454
|
+
detectorLang,
|
|
455
|
+
options.languages,
|
|
456
|
+
);
|
|
457
|
+
if (supportedLang) {
|
|
458
|
+
return supportedLang;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Fallback to instance's current language if detector didn't detect
|
|
463
|
+
if (detectorInstance.isInitialized && detectorInstance.language) {
|
|
464
|
+
const currentLang = detectorInstance.language;
|
|
465
|
+
if (isLanguageSupported(currentLang, options.languages)) {
|
|
466
|
+
return currentLang;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Silently ignore errors
|
|
471
|
+
} finally {
|
|
472
|
+
// Clean up temporary instance to avoid affecting subsequent formal initialization
|
|
473
|
+
if (isTemporary && detectorInstance !== i18nInstance) {
|
|
474
|
+
// Temporary instance is saved in cache for reuse
|
|
475
|
+
detectorInstanceCache.set(i18nInstance, {
|
|
476
|
+
instance: detectorInstance,
|
|
477
|
+
isTemporary: true,
|
|
478
|
+
configKey: buildDetectorConfigKey(
|
|
479
|
+
options.languages,
|
|
480
|
+
options.fallbackLanguage,
|
|
481
|
+
mergedDetection,
|
|
482
|
+
),
|
|
483
|
+
});
|
|
484
|
+
} else if (detectorInstance === i18nInstance) {
|
|
485
|
+
// As a fallback, prevent i18nInstance from being polluted by detector init
|
|
486
|
+
(i18nInstance as any).isInitialized = false;
|
|
487
|
+
delete (i18nInstance as any).language;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return undefined;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Detect language with priority:
|
|
496
|
+
* Priority 1: SSR data (try window._SSR_DATA first, works for both SSR and CSR)
|
|
497
|
+
* Priority 2: Path detection
|
|
498
|
+
* Priority 3: i18next detector (reads from cookie/localStorage)
|
|
499
|
+
* Priority 4: User config language or fallback
|
|
500
|
+
*/
|
|
501
|
+
export const detectLanguageWithPriority = async (
|
|
502
|
+
i18nInstance: I18nInstance,
|
|
503
|
+
options: LanguageDetectionOptions,
|
|
504
|
+
): Promise<LanguageDetectionResult> => {
|
|
505
|
+
const {
|
|
506
|
+
languages,
|
|
507
|
+
fallbackLanguage,
|
|
508
|
+
localePathRedirect,
|
|
509
|
+
i18nextDetector,
|
|
510
|
+
detection,
|
|
511
|
+
userInitOptions,
|
|
512
|
+
pathname,
|
|
513
|
+
ssrContext,
|
|
514
|
+
} = options;
|
|
515
|
+
|
|
516
|
+
let detectedLanguage: string | undefined;
|
|
517
|
+
|
|
518
|
+
// Priority 1: Try SSR data first (works for both SSR and CSR projects)
|
|
519
|
+
// For CSR projects, if SSR data exists in window, use it; otherwise continue to next priority
|
|
520
|
+
detectedLanguage = detectLanguageFromSSR(languages);
|
|
521
|
+
|
|
522
|
+
// Priority 2: Path detection
|
|
523
|
+
if (!detectedLanguage) {
|
|
524
|
+
detectedLanguage = detectLanguageFromPathPriority(
|
|
525
|
+
pathname,
|
|
526
|
+
languages,
|
|
527
|
+
localePathRedirect,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Priority 3: i18next detector (reads from cookie/localStorage)
|
|
532
|
+
if (!detectedLanguage && i18nextDetector) {
|
|
533
|
+
if (isI18nWrapperInstance(i18nInstance)) {
|
|
534
|
+
detectedLanguage = readLanguageFromStorage(
|
|
535
|
+
mergeDetectionOptions(
|
|
536
|
+
i18nextDetector,
|
|
537
|
+
detection,
|
|
538
|
+
localePathRedirect,
|
|
539
|
+
userInitOptions,
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
} else {
|
|
543
|
+
detectedLanguage = await detectLanguageFromI18nextDetector(i18nInstance, {
|
|
544
|
+
languages,
|
|
545
|
+
fallbackLanguage,
|
|
546
|
+
localePathRedirect,
|
|
547
|
+
i18nextDetector,
|
|
548
|
+
detection,
|
|
549
|
+
userInitOptions,
|
|
550
|
+
mergedBackend: options.mergedBackend,
|
|
551
|
+
ssrContext,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Priority 4: Use user config language or fallback
|
|
557
|
+
const finalLanguage =
|
|
558
|
+
detectedLanguage || userInitOptions?.lng || fallbackLanguage;
|
|
559
|
+
|
|
560
|
+
return { detectedLanguage, finalLanguage };
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Options for building i18n init options
|
|
565
|
+
*/
|
|
566
|
+
export interface BuildInitOptionsParams {
|
|
567
|
+
finalLanguage: string;
|
|
568
|
+
fallbackLanguage: string;
|
|
569
|
+
languages: string[];
|
|
570
|
+
userInitOptions?: I18nInitOptions;
|
|
571
|
+
mergedDetection?: any;
|
|
572
|
+
mergeBackend?: any;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Build i18n initialization options
|
|
577
|
+
*/
|
|
578
|
+
export const buildInitOptions = (
|
|
579
|
+
params: BuildInitOptionsParams,
|
|
580
|
+
): I18nInitOptions => {
|
|
581
|
+
const {
|
|
582
|
+
finalLanguage,
|
|
583
|
+
fallbackLanguage,
|
|
584
|
+
languages,
|
|
585
|
+
userInitOptions,
|
|
586
|
+
mergedDetection,
|
|
587
|
+
mergeBackend,
|
|
588
|
+
} = params;
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
...(userInitOptions || {}),
|
|
592
|
+
lng: finalLanguage,
|
|
593
|
+
fallbackLng: fallbackLanguage,
|
|
594
|
+
supportedLngs: languages,
|
|
595
|
+
detection: mergedDetection,
|
|
596
|
+
backend: mergeBackend,
|
|
597
|
+
interpolation: {
|
|
598
|
+
...(userInitOptions?.interpolation || {}),
|
|
599
|
+
escapeValue: userInitOptions?.interpolation?.escapeValue ?? false,
|
|
600
|
+
},
|
|
601
|
+
react: {
|
|
602
|
+
useSuspense: isBrowser(),
|
|
603
|
+
},
|
|
604
|
+
} as any;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Merge detection and backend options
|
|
609
|
+
*/
|
|
610
|
+
export const mergeDetectionOptions = (
|
|
611
|
+
i18nextDetector: boolean,
|
|
612
|
+
detection?: LanguageDetectorOptions,
|
|
613
|
+
localePathRedirect?: boolean,
|
|
614
|
+
userInitOptions?: I18nInitOptions,
|
|
615
|
+
) => {
|
|
616
|
+
// Exclude 'path' from detection order to avoid conflict with manual path detection
|
|
617
|
+
let mergedDetection: LanguageDetectorOptions;
|
|
618
|
+
if (i18nextDetector) {
|
|
619
|
+
// mergeDetectionOptionsUtil always returns an object with default options
|
|
620
|
+
mergedDetection = mergeDetectionOptionsUtil(
|
|
621
|
+
detection,
|
|
622
|
+
userInitOptions?.detection,
|
|
623
|
+
);
|
|
624
|
+
} else {
|
|
625
|
+
// If detector is disabled, use user options or empty object
|
|
626
|
+
mergedDetection = userInitOptions?.detection || {};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Ensure mergedDetection is always an object (should not be undefined after above)
|
|
630
|
+
if (!mergedDetection || typeof mergedDetection !== 'object') {
|
|
631
|
+
mergedDetection = {};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (localePathRedirect && mergedDetection.order) {
|
|
635
|
+
mergedDetection.order = mergedDetection.order.filter(
|
|
636
|
+
(item: string) => item !== 'path',
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return mergedDetection;
|
|
641
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { LanguageDetector } from 'i18next-http-middleware';
|
|
2
|
+
import type { I18nInstance } from '../instance';
|
|
3
|
+
|
|
4
|
+
export const cacheUserLanguage = (
|
|
5
|
+
_i18nInstance: I18nInstance,
|
|
6
|
+
_language: string,
|
|
7
|
+
_detectionOptions?: any,
|
|
8
|
+
): void => {
|
|
9
|
+
return;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read language directly from storage (localStorage/cookie)
|
|
14
|
+
* Not available in Node.js environment, returns undefined
|
|
15
|
+
*/
|
|
16
|
+
export const readLanguageFromStorage = (
|
|
17
|
+
_detectionOptions?: any,
|
|
18
|
+
): string | undefined => {
|
|
19
|
+
// In Node.js environment, storage-based detection is not available
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Register LanguageDetector plugin to i18n instance
|
|
24
|
+
* Must be called before init() to properly register the detector
|
|
25
|
+
*/
|
|
26
|
+
export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
|
|
27
|
+
if (!i18nInstance.isInitialized) {
|
|
28
|
+
return i18nInstance.use(LanguageDetector);
|
|
29
|
+
}
|
|
30
|
+
return i18nInstance;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect language using i18next-http-middleware LanguageDetector
|
|
35
|
+
* For initialized instances without detector in services, manually create a detector instance
|
|
36
|
+
*/
|
|
37
|
+
export const detectLanguage = (
|
|
38
|
+
i18nInstance: I18nInstance,
|
|
39
|
+
request?: any,
|
|
40
|
+
detectionOptions?: any,
|
|
41
|
+
): string | undefined => {
|
|
42
|
+
if (!request) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const detector = i18nInstance.services?.languageDetector;
|
|
48
|
+
if (detector && typeof detector.detect === 'function') {
|
|
49
|
+
const result = detector.detect(request, {});
|
|
50
|
+
if (typeof result === 'string') {
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
54
|
+
return result[0];
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
i18nInstance.isInitialized &&
|
|
61
|
+
i18nInstance.services &&
|
|
62
|
+
i18nInstance.options
|
|
63
|
+
) {
|
|
64
|
+
const manualDetector = new LanguageDetector();
|
|
65
|
+
const optionsToUse = detectionOptions
|
|
66
|
+
? { ...i18nInstance.options, detection: detectionOptions }
|
|
67
|
+
: i18nInstance.options;
|
|
68
|
+
manualDetector.init(i18nInstance.services, optionsToUse as any);
|
|
69
|
+
|
|
70
|
+
const result = (manualDetector.detect as any)(request, {}, undefined);
|
|
71
|
+
if (typeof result === 'string') {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
75
|
+
return result[0];
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|