@modern-js/plugin-i18n 2.69.5 → 3.0.0-alpha.0
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 +162 -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 +119 -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 +119 -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 +19 -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 +275 -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,333 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { BaseBackendOptions } from '../../shared/type';
|
|
3
|
+
import { mergeBackendOptions } from './backend';
|
|
4
|
+
import { HttpBackendWithSave } from './backend/middleware';
|
|
5
|
+
import { useI18nextBackend } from './backend/middleware';
|
|
6
|
+
import { SdkBackend } from './backend/sdk-backend';
|
|
7
|
+
import { cacheUserLanguage } from './detection';
|
|
8
|
+
import { mergeDetectionOptions } from './detection';
|
|
9
|
+
import type { I18nInitOptions, I18nInstance } from './instance';
|
|
10
|
+
import {
|
|
11
|
+
getActualI18nextInstance,
|
|
12
|
+
isI18nInstance,
|
|
13
|
+
isI18nWrapperInstance,
|
|
14
|
+
} from './instance';
|
|
15
|
+
|
|
16
|
+
export function assertI18nInstance(obj: any): asserts obj is I18nInstance {
|
|
17
|
+
if (!isI18nInstance(obj)) {
|
|
18
|
+
throw new Error('Object does not implement I18nInstance interface');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build initialization options for i18n instance
|
|
24
|
+
*/
|
|
25
|
+
export const buildInitOptions = async (
|
|
26
|
+
finalLanguage: string,
|
|
27
|
+
fallbackLanguage: string,
|
|
28
|
+
languages: string[],
|
|
29
|
+
mergedDetection: any,
|
|
30
|
+
mergedBackend: any,
|
|
31
|
+
userInitOptions?: I18nInitOptions,
|
|
32
|
+
useSuspense?: boolean,
|
|
33
|
+
i18nInstance?: I18nInstance,
|
|
34
|
+
): Promise<I18nInitOptions> => {
|
|
35
|
+
const defaultUseSuspense =
|
|
36
|
+
useSuspense !== undefined
|
|
37
|
+
? useSuspense
|
|
38
|
+
: isBrowser()
|
|
39
|
+
? (userInitOptions?.react?.useSuspense ?? true)
|
|
40
|
+
: false;
|
|
41
|
+
|
|
42
|
+
// If backend is already configured via useI18nextBackend (has _useChainedBackend),
|
|
43
|
+
// we need to pass the chained backend config to init() so it can initialize properly
|
|
44
|
+
const isChainedBackend = !!mergedBackend?._useChainedBackend;
|
|
45
|
+
|
|
46
|
+
// If using chained backend, we need to pass the backend config to init()
|
|
47
|
+
// but exclude it from userInitOptions to avoid conflicts
|
|
48
|
+
// For non-chained backend, we also exclude it to ensure mergedBackend is used
|
|
49
|
+
const sanitizedUserInitOptions = userInitOptions
|
|
50
|
+
? { ...userInitOptions, backend: undefined }
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
// Build base initOptions first, excluding backend to set it separately
|
|
54
|
+
const { backend: _removedBackend, ...userOptionsWithoutBackend } =
|
|
55
|
+
sanitizedUserInitOptions || {};
|
|
56
|
+
|
|
57
|
+
const initOptions: I18nInitOptions = {
|
|
58
|
+
lng: finalLanguage,
|
|
59
|
+
fallbackLng: fallbackLanguage,
|
|
60
|
+
supportedLngs: languages,
|
|
61
|
+
detection: mergedDetection,
|
|
62
|
+
initImmediate: sanitizedUserInitOptions?.initImmediate ?? true,
|
|
63
|
+
interpolation: {
|
|
64
|
+
...(sanitizedUserInitOptions?.interpolation || {}),
|
|
65
|
+
escapeValue:
|
|
66
|
+
sanitizedUserInitOptions?.interpolation?.escapeValue ?? false,
|
|
67
|
+
},
|
|
68
|
+
react: {
|
|
69
|
+
...(sanitizedUserInitOptions?.react || {}),
|
|
70
|
+
useSuspense: defaultUseSuspense,
|
|
71
|
+
},
|
|
72
|
+
// Spread user options (without backend) to allow user options to override
|
|
73
|
+
...userOptionsWithoutBackend,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// For chained backend, we need to pass the backend config to init()
|
|
77
|
+
// The backend classes (Backend, SdkBackend) are already set via useI18nextBackend
|
|
78
|
+
// but we need to pass the complete chained backend config to init()
|
|
79
|
+
// IMPORTANT: For i18next-chained-backend, we need to pass backends array in init() options
|
|
80
|
+
// because ChainedBackend reads it from initOptions.backend.backends during initialization
|
|
81
|
+
// IMPORTANT: For non-chained backend, we need to pass the backend config to init() so i18next
|
|
82
|
+
// can load resources from the configured loadPath
|
|
83
|
+
// IMPORTANT: Set backend config AFTER spreading user options to ensure it's not overridden
|
|
84
|
+
if (mergedBackend) {
|
|
85
|
+
if (isChainedBackend && mergedBackend._chainedBackendConfig) {
|
|
86
|
+
// Try to get backend classes from i18nInstance.options.backend.backends first
|
|
87
|
+
// This avoids importing fs-backend in browser environment
|
|
88
|
+
let HttpBackend: any;
|
|
89
|
+
let SdkBackendClass: any;
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
i18nInstance?.options?.backend?.backends &&
|
|
93
|
+
Array.isArray(i18nInstance.options.backend.backends) &&
|
|
94
|
+
i18nInstance.options.backend.backends.length >= 2
|
|
95
|
+
) {
|
|
96
|
+
// Use the backend classes already set by useI18nextBackend
|
|
97
|
+
HttpBackend = i18nInstance.options.backend.backends[0];
|
|
98
|
+
SdkBackendClass = i18nInstance.options.backend.backends[1];
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback: use backend classes from middleware
|
|
101
|
+
// Build tools will automatically select the correct file (.node.ts for Node.js, .ts for browser)
|
|
102
|
+
// HttpBackendWithSave is exported from both middleware.ts (browser) and middleware.node.ts (Node.js)
|
|
103
|
+
HttpBackend = HttpBackendWithSave;
|
|
104
|
+
SdkBackendClass = SdkBackend;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For chained backend, pass the complete chained backend config structure
|
|
108
|
+
// Note: HttpBackend and SdkBackendClass are already wrapped
|
|
109
|
+
// with save methods to ensure i18next-chained-backend's refresh logic is triggered
|
|
110
|
+
initOptions.backend = {
|
|
111
|
+
backends: [HttpBackend, SdkBackendClass],
|
|
112
|
+
backendOptions: mergedBackend._chainedBackendConfig.backendOptions,
|
|
113
|
+
cacheHitMode: mergedBackend.cacheHitMode || 'refreshAndUpdateStore',
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
// For non-chained backend, pass the backend config directly
|
|
117
|
+
// This ensures i18next can load resources from the configured loadPath
|
|
118
|
+
// Remove internal properties (_useChainedBackend, _chainedBackendConfig) before passing to init()
|
|
119
|
+
const { _useChainedBackend, _chainedBackendConfig, ...cleanBackend } =
|
|
120
|
+
mergedBackend || {};
|
|
121
|
+
initOptions.backend = cleanBackend;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return initOptions;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ensure i18n instance language matches the final detected language
|
|
130
|
+
*/
|
|
131
|
+
export const ensureLanguageMatch = async (
|
|
132
|
+
i18nInstance: I18nInstance,
|
|
133
|
+
finalLanguage: string,
|
|
134
|
+
): Promise<void> => {
|
|
135
|
+
if (i18nInstance.language !== finalLanguage) {
|
|
136
|
+
await i18nInstance.setLang?.(finalLanguage);
|
|
137
|
+
await i18nInstance.changeLanguage?.(finalLanguage);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Change language for i18n instance in onBeforeRender hook
|
|
143
|
+
* This function can be used by other runtime plugins to change language
|
|
144
|
+
* @param i18nInstance - The i18n instance
|
|
145
|
+
* @param newLang - The new language code to switch to
|
|
146
|
+
* @param options - Optional configuration
|
|
147
|
+
*/
|
|
148
|
+
export const changeI18nLanguage = async (
|
|
149
|
+
i18nInstance: I18nInstance,
|
|
150
|
+
newLang: string,
|
|
151
|
+
options?: {
|
|
152
|
+
detectionOptions?: any;
|
|
153
|
+
},
|
|
154
|
+
): Promise<void> => {
|
|
155
|
+
if (!newLang || typeof newLang !== 'string') {
|
|
156
|
+
throw new Error('Language must be a non-empty string');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!i18nInstance) {
|
|
160
|
+
throw new Error('i18nInstance is required');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update i18n instance language
|
|
164
|
+
await i18nInstance.setLang?.(newLang);
|
|
165
|
+
await i18nInstance.changeLanguage?.(newLang);
|
|
166
|
+
|
|
167
|
+
// Cache language in browser environment
|
|
168
|
+
if (isBrowser()) {
|
|
169
|
+
const detectionOptions =
|
|
170
|
+
options?.detectionOptions || i18nInstance.options?.detection;
|
|
171
|
+
cacheUserLanguage(i18nInstance, newLang, detectionOptions);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Initialize i18n instance if not already initialized
|
|
177
|
+
*/
|
|
178
|
+
export const initializeI18nInstance = async (
|
|
179
|
+
i18nInstance: I18nInstance,
|
|
180
|
+
finalLanguage: string,
|
|
181
|
+
fallbackLanguage: string,
|
|
182
|
+
languages: string[],
|
|
183
|
+
mergedDetection: any,
|
|
184
|
+
mergedBackend: any,
|
|
185
|
+
userInitOptions?: I18nInitOptions,
|
|
186
|
+
useSuspense?: boolean,
|
|
187
|
+
): Promise<void> => {
|
|
188
|
+
if (!i18nInstance.isInitialized) {
|
|
189
|
+
const initOptions = await buildInitOptions(
|
|
190
|
+
finalLanguage,
|
|
191
|
+
fallbackLanguage,
|
|
192
|
+
languages,
|
|
193
|
+
mergedDetection,
|
|
194
|
+
mergedBackend,
|
|
195
|
+
userInitOptions,
|
|
196
|
+
useSuspense,
|
|
197
|
+
i18nInstance,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// For i18next, backend configuration must be passed to init() via initOptions.backend
|
|
201
|
+
// The backend class is already registered via useI18nextBackend, but the config (loadPath, etc.)
|
|
202
|
+
// needs to be in initOptions.backend for init() to use it
|
|
203
|
+
const actualInstance = getActualI18nextInstance(i18nInstance);
|
|
204
|
+
const savedBackendConfig =
|
|
205
|
+
actualInstance?.options?.backend || i18nInstance.options?.backend;
|
|
206
|
+
const isChainedBackendFromSaved =
|
|
207
|
+
savedBackendConfig?.backends &&
|
|
208
|
+
Array.isArray(savedBackendConfig.backends);
|
|
209
|
+
|
|
210
|
+
await i18nInstance.init(initOptions);
|
|
211
|
+
|
|
212
|
+
if (mergedBackend) {
|
|
213
|
+
if (isI18nWrapperInstance(i18nInstance) && actualInstance?.options) {
|
|
214
|
+
if (isChainedBackendFromSaved && initOptions.backend) {
|
|
215
|
+
actualInstance.options.backend = {
|
|
216
|
+
...initOptions.backend,
|
|
217
|
+
backends: savedBackendConfig.backends,
|
|
218
|
+
};
|
|
219
|
+
} else if (initOptions.backend) {
|
|
220
|
+
actualInstance.options.backend = {
|
|
221
|
+
...actualInstance.options.backend,
|
|
222
|
+
...initOptions.backend,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (hasOptions(i18nInstance)) {
|
|
228
|
+
if (isChainedBackendFromSaved && initOptions.backend) {
|
|
229
|
+
i18nInstance.options.backend = {
|
|
230
|
+
...initOptions.backend,
|
|
231
|
+
backends: savedBackendConfig.backends,
|
|
232
|
+
};
|
|
233
|
+
} else if (initOptions.backend) {
|
|
234
|
+
i18nInstance.options.backend = {
|
|
235
|
+
...i18nInstance.options.backend,
|
|
236
|
+
...initOptions.backend,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (mergedBackend && hasOptions(i18nInstance)) {
|
|
243
|
+
// For chained backend with cacheHitMode: 'refreshAndUpdateStore',
|
|
244
|
+
// i18next-chained-backend automatically:
|
|
245
|
+
// 1. Loads from the first backend (HTTP/FS) and displays immediately
|
|
246
|
+
// 2. Asynchronously loads from the second backend (SDK) and updates the store
|
|
247
|
+
// 3. Triggers 'loaded' event when SDK resources are loaded, which causes React to re-render
|
|
248
|
+
//
|
|
249
|
+
// Note: i18next.init() returns a Promise that resolves when the first backend loads.
|
|
250
|
+
// For chained backend, it does NOT wait for the second backend (SDK) to load.
|
|
251
|
+
// The SDK backend loads asynchronously and triggers 'loaded' event automatically.
|
|
252
|
+
const defaultNS =
|
|
253
|
+
initOptions.defaultNS || initOptions.ns || 'translation';
|
|
254
|
+
const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
|
|
255
|
+
|
|
256
|
+
let retries = 20;
|
|
257
|
+
while (retries > 0) {
|
|
258
|
+
// Get the actual i18next instance to access store property
|
|
259
|
+
const actualInstance = getActualI18nextInstance(i18nInstance);
|
|
260
|
+
const store = (actualInstance as any).store;
|
|
261
|
+
if (store?.data?.[finalLanguage]?.[ns]) {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
265
|
+
retries--;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Type guard to check if i18n instance has options property
|
|
273
|
+
*/
|
|
274
|
+
function hasOptions(instance: I18nInstance): instance is I18nInstance & {
|
|
275
|
+
options: NonNullable<I18nInstance['options']>;
|
|
276
|
+
} {
|
|
277
|
+
return instance.options !== undefined && instance.options !== null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Setup cloned instance for SSR with backend support
|
|
282
|
+
*/
|
|
283
|
+
export const setupClonedInstance = async (
|
|
284
|
+
i18nInstance: I18nInstance,
|
|
285
|
+
finalLanguage: string,
|
|
286
|
+
fallbackLanguage: string,
|
|
287
|
+
languages: string[],
|
|
288
|
+
backendEnabled: boolean,
|
|
289
|
+
backend: BaseBackendOptions | undefined,
|
|
290
|
+
i18nextDetector: boolean,
|
|
291
|
+
detection: any,
|
|
292
|
+
localePathRedirect: boolean,
|
|
293
|
+
userInitOptions: I18nInitOptions | undefined,
|
|
294
|
+
): Promise<void> => {
|
|
295
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
296
|
+
// Check if SDK is configured (allows standalone SDK usage even without locales directory)
|
|
297
|
+
const hasSdkConfig =
|
|
298
|
+
typeof userInitOptions?.backend?.sdk === 'function' ||
|
|
299
|
+
(mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
|
|
300
|
+
|
|
301
|
+
if (backendEnabled || hasSdkConfig) {
|
|
302
|
+
useI18nextBackend(i18nInstance, mergedBackend);
|
|
303
|
+
if (mergedBackend && hasOptions(i18nInstance)) {
|
|
304
|
+
i18nInstance.options.backend = {
|
|
305
|
+
...i18nInstance.options.backend,
|
|
306
|
+
...mergedBackend,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (i18nInstance.isInitialized) {
|
|
311
|
+
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
312
|
+
} else {
|
|
313
|
+
const mergedDetection = mergeDetectionOptions(
|
|
314
|
+
i18nextDetector,
|
|
315
|
+
detection,
|
|
316
|
+
localePathRedirect,
|
|
317
|
+
userInitOptions,
|
|
318
|
+
);
|
|
319
|
+
await initializeI18nInstance(
|
|
320
|
+
i18nInstance,
|
|
321
|
+
finalLanguage,
|
|
322
|
+
fallbackLanguage,
|
|
323
|
+
languages,
|
|
324
|
+
mergedDetection,
|
|
325
|
+
mergedBackend,
|
|
326
|
+
userInitOptions,
|
|
327
|
+
false, // SSR always uses false for useSuspense
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RuntimePlugin,
|
|
3
|
+
isBrowser,
|
|
4
|
+
useRuntimeContext,
|
|
5
|
+
} from '@modern-js/runtime';
|
|
6
|
+
import { merge } from '@modern-js/runtime-utils/merge';
|
|
7
|
+
import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
|
|
8
|
+
import type React from 'react';
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import type {
|
|
11
|
+
BaseBackendOptions,
|
|
12
|
+
BaseLocaleDetectionOptions,
|
|
13
|
+
} from '../shared/type';
|
|
14
|
+
import { ModernI18nProvider } from './context';
|
|
15
|
+
import {
|
|
16
|
+
createContextValue,
|
|
17
|
+
useClientSideRedirect,
|
|
18
|
+
useLanguageSync,
|
|
19
|
+
useSdkResourcesLoader,
|
|
20
|
+
} from './hooks';
|
|
21
|
+
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
22
|
+
import { getI18nInstance } from './i18n';
|
|
23
|
+
import { mergeBackendOptions } from './i18n/backend';
|
|
24
|
+
import { useI18nextBackend } from './i18n/backend/middleware';
|
|
25
|
+
import {
|
|
26
|
+
detectLanguageWithPriority,
|
|
27
|
+
exportServerLngToWindow,
|
|
28
|
+
mergeDetectionOptions,
|
|
29
|
+
} from './i18n/detection';
|
|
30
|
+
import { useI18nextLanguageDetector } from './i18n/detection/middleware';
|
|
31
|
+
import {
|
|
32
|
+
getI18nextInstanceForProvider,
|
|
33
|
+
getI18nextProvider,
|
|
34
|
+
getInitReactI18next,
|
|
35
|
+
} from './i18n/instance';
|
|
36
|
+
import {
|
|
37
|
+
changeI18nLanguage,
|
|
38
|
+
ensureLanguageMatch,
|
|
39
|
+
initializeI18nInstance,
|
|
40
|
+
setupClonedInstance,
|
|
41
|
+
} from './i18n/utils';
|
|
42
|
+
import { getPathname } from './utils';
|
|
43
|
+
import './types';
|
|
44
|
+
|
|
45
|
+
export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
|
|
46
|
+
export type { Resources } from './i18n/instance';
|
|
47
|
+
|
|
48
|
+
export interface I18nPluginOptions {
|
|
49
|
+
entryName?: string;
|
|
50
|
+
localeDetection?: BaseLocaleDetectionOptions;
|
|
51
|
+
backend?: BaseBackendOptions;
|
|
52
|
+
i18nInstance?: I18nInstance;
|
|
53
|
+
changeLanguage?: (lang: string) => void;
|
|
54
|
+
initOptions?: I18nInitOptions;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RuntimeContextWithI18n extends TInternalRuntimeContext {
|
|
59
|
+
i18nInstance?: I18nInstance;
|
|
60
|
+
changeLanguage?: (lang: string) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
64
|
+
name: '@modern-js/plugin-i18n',
|
|
65
|
+
setup: api => {
|
|
66
|
+
const {
|
|
67
|
+
entryName,
|
|
68
|
+
i18nInstance: userI18nInstance,
|
|
69
|
+
initOptions,
|
|
70
|
+
localeDetection,
|
|
71
|
+
backend,
|
|
72
|
+
} = options;
|
|
73
|
+
const {
|
|
74
|
+
localePathRedirect = false,
|
|
75
|
+
i18nextDetector = true,
|
|
76
|
+
languages = [],
|
|
77
|
+
fallbackLanguage = 'en',
|
|
78
|
+
detection,
|
|
79
|
+
ignoreRedirectRoutes,
|
|
80
|
+
} = localeDetection || {};
|
|
81
|
+
const { enabled: backendEnabled = false } = backend || {};
|
|
82
|
+
let I18nextProvider: React.FunctionComponent<any> | null;
|
|
83
|
+
|
|
84
|
+
api.onBeforeRender(async context => {
|
|
85
|
+
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
86
|
+
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
87
|
+
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
88
|
+
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
89
|
+
const initReactI18next = await getInitReactI18next();
|
|
90
|
+
I18nextProvider = await getI18nextProvider();
|
|
91
|
+
if (initReactI18next) {
|
|
92
|
+
i18nInstance.use(initReactI18next);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pathname = getPathname(context);
|
|
96
|
+
|
|
97
|
+
if (i18nextDetector) {
|
|
98
|
+
useI18nextLanguageDetector(i18nInstance);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const mergedDetection = mergeDetectionOptions(
|
|
102
|
+
i18nextDetector,
|
|
103
|
+
detection,
|
|
104
|
+
localePathRedirect,
|
|
105
|
+
userInitOptions,
|
|
106
|
+
);
|
|
107
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
108
|
+
|
|
109
|
+
// Register Backend BEFORE detectLanguageWithPriority
|
|
110
|
+
// This is critical because detectLanguageWithPriority may trigger init()
|
|
111
|
+
// through i18next detector, and backend must be registered before init()
|
|
112
|
+
// Register backend if:
|
|
113
|
+
// 1. enabled is true (explicitly or auto-detected), OR
|
|
114
|
+
// 2. SDK is configured (allows standalone SDK usage even without locales directory)
|
|
115
|
+
const hasSdkConfig =
|
|
116
|
+
typeof userInitOptions?.backend?.sdk === 'function' ||
|
|
117
|
+
(mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
|
|
118
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) {
|
|
119
|
+
useI18nextBackend(i18nInstance, mergedBackend);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
123
|
+
languages,
|
|
124
|
+
fallbackLanguage,
|
|
125
|
+
localePathRedirect,
|
|
126
|
+
i18nextDetector,
|
|
127
|
+
detection,
|
|
128
|
+
userInitOptions,
|
|
129
|
+
mergedBackend,
|
|
130
|
+
pathname,
|
|
131
|
+
ssrContext: context.ssrContext,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await initializeI18nInstance(
|
|
135
|
+
i18nInstance,
|
|
136
|
+
finalLanguage,
|
|
137
|
+
fallbackLanguage,
|
|
138
|
+
languages,
|
|
139
|
+
mergedDetection,
|
|
140
|
+
mergedBackend,
|
|
141
|
+
userInitOptions,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
145
|
+
i18nInstance = i18nInstance.cloneInstance();
|
|
146
|
+
await setupClonedInstance(
|
|
147
|
+
i18nInstance,
|
|
148
|
+
finalLanguage,
|
|
149
|
+
fallbackLanguage,
|
|
150
|
+
languages,
|
|
151
|
+
backendEnabled,
|
|
152
|
+
backend,
|
|
153
|
+
i18nextDetector,
|
|
154
|
+
detection,
|
|
155
|
+
localePathRedirect,
|
|
156
|
+
userInitOptions,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (localePathRedirect) {
|
|
161
|
+
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isBrowser()) {
|
|
165
|
+
exportServerLngToWindow(context, finalLanguage);
|
|
166
|
+
}
|
|
167
|
+
context.i18nInstance = i18nInstance;
|
|
168
|
+
|
|
169
|
+
// Add changeLanguage method to context for other runtime plugins to use
|
|
170
|
+
context.changeLanguage = async (newLang: string) => {
|
|
171
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
172
|
+
detectionOptions: mergedDetection,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
api.wrapRoot(App => {
|
|
178
|
+
return props => {
|
|
179
|
+
const runtimeContext = useRuntimeContext() as RuntimeContextWithI18n;
|
|
180
|
+
const i18nInstance = runtimeContext.i18nInstance;
|
|
181
|
+
const initialLang = useMemo(
|
|
182
|
+
() =>
|
|
183
|
+
i18nInstance?.language ||
|
|
184
|
+
(localeDetection?.fallbackLanguage ?? 'en'),
|
|
185
|
+
[i18nInstance?.language, localeDetection?.fallbackLanguage],
|
|
186
|
+
);
|
|
187
|
+
const [lang, setLang] = useState(initialLang);
|
|
188
|
+
const [forceUpdate, setForceUpdate] = useState(0);
|
|
189
|
+
const prevLangRef = useRef(lang);
|
|
190
|
+
const runtimeContextRef = useRef(runtimeContext);
|
|
191
|
+
runtimeContextRef.current = runtimeContext;
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (i18nInstance?.language) {
|
|
195
|
+
const translator = (i18nInstance as any).translator;
|
|
196
|
+
if (translator) {
|
|
197
|
+
translator.language = i18nInstance.language;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, [i18nInstance?.language]);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
prevLangRef.current = lang;
|
|
204
|
+
}, [lang]);
|
|
205
|
+
|
|
206
|
+
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
207
|
+
useLanguageSync(
|
|
208
|
+
i18nInstance,
|
|
209
|
+
localePathRedirect,
|
|
210
|
+
languages,
|
|
211
|
+
runtimeContextRef,
|
|
212
|
+
prevLangRef,
|
|
213
|
+
setLang,
|
|
214
|
+
);
|
|
215
|
+
// Handle client-side redirect for static deployments
|
|
216
|
+
// Note: This hook only executes in browser environment and skips SSR scenarios
|
|
217
|
+
useClientSideRedirect(
|
|
218
|
+
i18nInstance,
|
|
219
|
+
localePathRedirect,
|
|
220
|
+
languages,
|
|
221
|
+
fallbackLanguage,
|
|
222
|
+
ignoreRedirectRoutes,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const contextValue = useMemo(
|
|
226
|
+
() =>
|
|
227
|
+
createContextValue(
|
|
228
|
+
lang,
|
|
229
|
+
i18nInstance,
|
|
230
|
+
entryName,
|
|
231
|
+
languages,
|
|
232
|
+
localePathRedirect,
|
|
233
|
+
ignoreRedirectRoutes,
|
|
234
|
+
setLang,
|
|
235
|
+
),
|
|
236
|
+
[
|
|
237
|
+
lang,
|
|
238
|
+
i18nInstance,
|
|
239
|
+
entryName,
|
|
240
|
+
languages,
|
|
241
|
+
localePathRedirect,
|
|
242
|
+
ignoreRedirectRoutes,
|
|
243
|
+
forceUpdate,
|
|
244
|
+
],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const appContent = (
|
|
248
|
+
<ModernI18nProvider value={contextValue}>
|
|
249
|
+
<App {...props} />
|
|
250
|
+
</ModernI18nProvider>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (!i18nInstance) {
|
|
254
|
+
return appContent;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (I18nextProvider) {
|
|
258
|
+
const i18nextInstanceForProvider =
|
|
259
|
+
getI18nextInstanceForProvider(i18nInstance);
|
|
260
|
+
return (
|
|
261
|
+
<I18nextProvider i18n={i18nextInstanceForProvider}>
|
|
262
|
+
{appContent}
|
|
263
|
+
</I18nextProvider>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return appContent;
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export { useModernI18n } from './context';
|
|
274
|
+
export { I18nLink } from './I18nLink';
|
|
275
|
+
export default i18nPlugin;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
2
|
+
|
|
3
|
+
declare module '@modern-js/runtime' {
|
|
4
|
+
interface RuntimeConfig {
|
|
5
|
+
i18n?: {
|
|
6
|
+
i18nInstance?: I18nInstance;
|
|
7
|
+
changeLanguage?: (lang: string) => void;
|
|
8
|
+
setLang?: (lang: string) => void;
|
|
9
|
+
initOptions?: I18nInitOptions;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TInternalRuntimeContext {
|
|
14
|
+
i18nInstance?: I18nInstance;
|
|
15
|
+
changeLanguage?: (lang: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
}
|