@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.30 → 3.5.0-ultramodern.31
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/index.js +2 -1
- package/dist/cjs/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +18 -82
- package/dist/cjs/runtime/contextHelpers.js +137 -0
- package/dist/cjs/runtime/core.js +29 -132
- package/dist/cjs/runtime/i18n/detection/cache.js +149 -0
- package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
- package/dist/cjs/runtime/i18n/detection/index.js +8 -263
- package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
- package/dist/cjs/runtime/i18n/detection/language.js +62 -0
- package/dist/cjs/runtime/i18n/detection/path.js +49 -0
- package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
- package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
- package/dist/cjs/runtime/i18n/detection/types.js +18 -0
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +113 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -152
- package/dist/cjs/server/redirectPolicy.js +102 -0
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +75 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
- package/dist/cjs/shared/localisedUrls/routes.js +129 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +13 -294
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +18 -82
- package/dist/esm/runtime/contextHelpers.mjs +84 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/i18n/detection/cache.mjs +102 -0
- package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
- package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
- package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
- package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
- package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +75 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +4 -139
- package/dist/esm/server/redirectPolicy.mjs +52 -0
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +6 -0
- package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +50 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +18 -82
- package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -0
- package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
- package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
- package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
- package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
- package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +76 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- package/dist/esm-node/server/apiPrefix.mjs +32 -0
- package/dist/esm-node/server/detectorOptions.mjs +60 -0
- package/dist/esm-node/server/index.mjs +4 -139
- package/dist/esm-node/server/redirectPolicy.mjs +53 -0
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +7 -0
- package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +51 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +2 -2
- package/dist/types/runtime/contextHelpers.d.ts +28 -0
- package/dist/types/runtime/core.d.ts +3 -7
- package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
- package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
- package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
- package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/types.d.ts +29 -0
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- package/dist/types/runtime/linkHelpers.d.ts +17 -0
- package/dist/types/runtime/pluginSetup.d.ts +32 -0
- package/dist/types/runtime/providerComposition.d.ts +15 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- package/dist/types/server/apiPrefix.d.ts +7 -0
- package/dist/types/server/detectorOptions.d.ts +21 -0
- package/dist/types/server/index.d.ts +1 -6
- package/dist/types/server/redirectPolicy.d.ts +16 -0
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +7 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +8 -0
- package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
- package/dist/types/shared/localisedUrls/types.d.ts +14 -0
- package/dist/types/shared/localisedUrls.d.ts +2 -42
- package/package.json +10 -10
- package/src/cli/index.ts +3 -2
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +42 -196
- package/src/runtime/contextHelpers.ts +232 -0
- package/src/runtime/core.tsx +42 -273
- package/src/runtime/i18n/detection/cache.ts +175 -0
- package/src/runtime/i18n/detection/detector.ts +157 -0
- package/src/runtime/i18n/detection/index.ts +10 -641
- package/src/runtime/i18n/detection/initOptions.ts +74 -0
- package/src/runtime/i18n/detection/language.ts +64 -0
- package/src/runtime/i18n/detection/path.ts +41 -0
- package/src/runtime/i18n/detection/priority.ts +82 -0
- package/src/runtime/i18n/detection/ssr.ts +47 -0
- package/src/runtime/i18n/detection/types.ts +33 -0
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +142 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +12 -327
- package/src/server/redirectPolicy.ts +152 -0
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +13 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +57 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/resolve.ts +118 -0
- package/src/shared/localisedUrls/routes.ts +226 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +20 -629
- package/tests/link.test.tsx +111 -0
- package/tests/localisedUrls.test.ts +50 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +5 -2
- package/tests/routerAdapter.test.tsx +56 -0
package/src/runtime/core.tsx
CHANGED
|
@@ -1,64 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isBrowser,
|
|
3
|
-
RuntimeContext,
|
|
4
|
-
type RuntimePlugin,
|
|
5
|
-
} from '@modern-js/runtime';
|
|
6
|
-
import { Helmet } from '@modern-js/runtime/head';
|
|
7
|
-
import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
|
|
8
|
-
import { merge } from '@modern-js/runtime-utils/merge';
|
|
1
|
+
import type { RuntimePlugin } from '@modern-js/runtime';
|
|
9
2
|
import type React from 'react';
|
|
10
|
-
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
3
|
import type {
|
|
12
4
|
BaseBackendOptions,
|
|
13
5
|
BaseLocaleDetectionOptions,
|
|
14
6
|
} from '../shared/type';
|
|
15
|
-
import { ModernI18nProvider } from './context';
|
|
16
|
-
import {
|
|
17
|
-
createContextValue,
|
|
18
|
-
useClientSideRedirect,
|
|
19
|
-
useLanguageSync,
|
|
20
|
-
useSdkResourcesLoader,
|
|
21
|
-
} from './hooks';
|
|
22
7
|
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
23
|
-
import { getI18nInstance } from './i18n';
|
|
24
|
-
import { mergeBackendOptions } from './i18n/backend';
|
|
25
8
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from './
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
9
|
+
type RuntimeContextWithI18n,
|
|
10
|
+
setupI18nBeforeRender,
|
|
11
|
+
} from './pluginSetup';
|
|
12
|
+
import { createI18nRootWrapper } from './providerComposition';
|
|
13
|
+
import {
|
|
14
|
+
type LoadReactI18nextIntegration,
|
|
15
|
+
resolveReactI18nextIntegration,
|
|
16
|
+
} from './reactI18next';
|
|
33
17
|
import './types';
|
|
34
18
|
|
|
35
19
|
export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
|
|
36
20
|
export type { Resources } from './i18n/instance';
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
|
|
42
|
-
initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
|
|
43
|
-
setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
|
|
47
|
-
|
|
48
|
-
function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
|
|
49
|
-
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
50
|
-
import('./i18n/backend/middleware'),
|
|
51
|
-
import('./i18n/utils'),
|
|
52
|
-
]).then(([backendMiddleware, utils]) => ({
|
|
53
|
-
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
54
|
-
changeI18nLanguage: utils.changeI18nLanguage,
|
|
55
|
-
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
56
|
-
initializeI18nInstance: utils.initializeI18nInstance,
|
|
57
|
-
setupClonedInstance: utils.setupClonedInstance,
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
return i18nLifecycleHelpersPromise;
|
|
61
|
-
}
|
|
21
|
+
export type {
|
|
22
|
+
LoadReactI18nextIntegration,
|
|
23
|
+
ReactI18nextIntegration,
|
|
24
|
+
} from './reactI18next';
|
|
62
25
|
|
|
63
26
|
export interface I18nPluginOptions {
|
|
64
27
|
entryName?: string;
|
|
@@ -72,19 +35,6 @@ export interface I18nPluginOptions {
|
|
|
72
35
|
[key: string]: any;
|
|
73
36
|
}
|
|
74
37
|
|
|
75
|
-
interface RuntimeContextWithI18n extends TInternalRuntimeContext {
|
|
76
|
-
i18nInstance?: I18nInstance;
|
|
77
|
-
changeLanguage?: (lang: string) => Promise<void>;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface ReactI18nextIntegration {
|
|
81
|
-
I18nextProvider: React.ComponentType<any> | null;
|
|
82
|
-
initReactI18next: any | null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export type LoadReactI18nextIntegration =
|
|
86
|
-
() => Promise<ReactI18nextIntegration | null>;
|
|
87
|
-
|
|
88
38
|
export const createI18nPlugin =
|
|
89
39
|
(
|
|
90
40
|
loadReactI18nextIntegration?: LoadReactI18nextIntegration,
|
|
@@ -114,223 +64,42 @@ export const createI18nPlugin =
|
|
|
114
64
|
let latestI18nInstance: I18nInstance | undefined;
|
|
115
65
|
let I18nextProvider: React.ComponentType<any> | null;
|
|
116
66
|
|
|
117
|
-
const resolveReactI18nextIntegration = async () => {
|
|
118
|
-
if (!reactI18next) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
return loadReactI18nextIntegration?.() ?? null;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
67
|
api.onBeforeRender(async (context: RuntimeContextWithI18n) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} = await loadI18nLifecycleHelpers();
|
|
132
|
-
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
133
|
-
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
134
|
-
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
135
|
-
const userInitOptions = merge(
|
|
136
|
-
otherInitOptions || {},
|
|
137
|
-
initOptions || {},
|
|
138
|
-
);
|
|
139
|
-
const reactI18nextIntegration = await resolveReactI18nextIntegration();
|
|
140
|
-
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
141
|
-
if (reactI18nextIntegration?.initReactI18next) {
|
|
142
|
-
i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const pathname = getPathname(context);
|
|
146
|
-
|
|
147
|
-
if (i18nextDetector) {
|
|
148
|
-
useI18nextLanguageDetector(i18nInstance);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const mergedDetection = mergeDetectionOptions(
|
|
68
|
+
latestI18nInstance = await setupI18nBeforeRender(context, {
|
|
69
|
+
api,
|
|
70
|
+
userI18nInstance,
|
|
71
|
+
initOptions,
|
|
72
|
+
backend,
|
|
73
|
+
backendEnabled,
|
|
152
74
|
i18nextDetector,
|
|
153
75
|
detection,
|
|
154
76
|
localePathRedirect,
|
|
155
|
-
userInitOptions,
|
|
156
|
-
);
|
|
157
|
-
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
158
|
-
|
|
159
|
-
// Register Backend BEFORE detectLanguageWithPriority
|
|
160
|
-
// This is critical because detectLanguageWithPriority may trigger init()
|
|
161
|
-
// through i18next detector, and backend must be registered before init()
|
|
162
|
-
// Register backend if:
|
|
163
|
-
// 1. enabled is true (explicitly or auto-detected), OR
|
|
164
|
-
// 2. SDK is configured (allows standalone SDK usage even without locales directory)
|
|
165
|
-
const hasSdkConfig =
|
|
166
|
-
typeof userInitOptions?.backend?.sdk === 'function' ||
|
|
167
|
-
(mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
|
|
168
|
-
if (mergedBackend && (backendEnabled || hasSdkConfig)) {
|
|
169
|
-
useI18nextBackend(i18nInstance, mergedBackend);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const { finalLanguage } = await detectLanguageWithPriority(
|
|
173
|
-
i18nInstance,
|
|
174
|
-
{
|
|
175
|
-
languages,
|
|
176
|
-
fallbackLanguage,
|
|
177
|
-
localePathRedirect,
|
|
178
|
-
i18nextDetector,
|
|
179
|
-
detection,
|
|
180
|
-
userInitOptions,
|
|
181
|
-
mergedBackend,
|
|
182
|
-
pathname,
|
|
183
|
-
ssrContext: context.ssrContext,
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
await initializeI18nInstance(
|
|
188
|
-
i18nInstance,
|
|
189
|
-
finalLanguage,
|
|
190
|
-
fallbackLanguage,
|
|
191
77
|
languages,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
fallbackLanguage,
|
|
203
|
-
languages,
|
|
204
|
-
backendEnabled,
|
|
205
|
-
backend,
|
|
206
|
-
i18nextDetector,
|
|
207
|
-
detection,
|
|
208
|
-
localePathRedirect,
|
|
209
|
-
userInitOptions,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (localePathRedirect) {
|
|
214
|
-
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!isBrowser()) {
|
|
218
|
-
exportServerLngToWindow(context, finalLanguage);
|
|
219
|
-
}
|
|
220
|
-
context.i18nInstance = i18nInstance;
|
|
221
|
-
latestI18nInstance = i18nInstance;
|
|
222
|
-
|
|
223
|
-
// Add changeLanguage method to context for other runtime plugins to use
|
|
224
|
-
context.changeLanguage = async (newLang: string) => {
|
|
225
|
-
await changeI18nLanguage(i18nInstance, newLang, {
|
|
226
|
-
detectionOptions: mergedDetection,
|
|
227
|
-
});
|
|
228
|
-
};
|
|
78
|
+
fallbackLanguage,
|
|
79
|
+
resolveReactI18nextIntegration: () =>
|
|
80
|
+
resolveReactI18nextIntegration(
|
|
81
|
+
reactI18next,
|
|
82
|
+
loadReactI18nextIntegration,
|
|
83
|
+
),
|
|
84
|
+
setI18nextProvider: provider => {
|
|
85
|
+
I18nextProvider = provider;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
229
88
|
});
|
|
230
89
|
|
|
231
|
-
api.wrapRoot(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const [lang, setLang] = useState(initialLang);
|
|
245
|
-
const [forceUpdate, setForceUpdate] = useState(0);
|
|
246
|
-
const prevLangRef = useRef(lang);
|
|
247
|
-
const runtimeContextRef = useRef(runtimeContext);
|
|
248
|
-
runtimeContextRef.current = runtimeContext;
|
|
249
|
-
|
|
250
|
-
useEffect(() => {
|
|
251
|
-
if (i18nInstance?.language) {
|
|
252
|
-
const translator = (i18nInstance as any).translator;
|
|
253
|
-
if (translator) {
|
|
254
|
-
translator.language = i18nInstance.language;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}, [i18nInstance?.language]);
|
|
258
|
-
|
|
259
|
-
useEffect(() => {
|
|
260
|
-
prevLangRef.current = lang;
|
|
261
|
-
}, [lang]);
|
|
262
|
-
|
|
263
|
-
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
264
|
-
useLanguageSync(
|
|
265
|
-
i18nInstance,
|
|
266
|
-
localePathRedirect,
|
|
267
|
-
languages,
|
|
268
|
-
runtimeContextRef,
|
|
269
|
-
prevLangRef,
|
|
270
|
-
setLang,
|
|
271
|
-
);
|
|
272
|
-
// Handle client-side redirect for static deployments
|
|
273
|
-
// Note: This hook only executes in browser environment and skips SSR scenarios
|
|
274
|
-
useClientSideRedirect(
|
|
275
|
-
i18nInstance,
|
|
276
|
-
localePathRedirect,
|
|
277
|
-
languages,
|
|
278
|
-
fallbackLanguage,
|
|
279
|
-
ignoreRedirectRoutes,
|
|
280
|
-
localisedUrls,
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const contextValue = useMemo(
|
|
284
|
-
() =>
|
|
285
|
-
createContextValue(
|
|
286
|
-
lang,
|
|
287
|
-
i18nInstance,
|
|
288
|
-
entryName,
|
|
289
|
-
languages,
|
|
290
|
-
localePathRedirect,
|
|
291
|
-
ignoreRedirectRoutes,
|
|
292
|
-
localisedUrls,
|
|
293
|
-
setLang,
|
|
294
|
-
),
|
|
295
|
-
[
|
|
296
|
-
lang,
|
|
297
|
-
i18nInstance,
|
|
298
|
-
entryName,
|
|
299
|
-
languages,
|
|
300
|
-
localePathRedirect,
|
|
301
|
-
ignoreRedirectRoutes,
|
|
302
|
-
localisedUrls,
|
|
303
|
-
forceUpdate,
|
|
304
|
-
],
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const children = (props as React.PropsWithChildren).children;
|
|
308
|
-
const appContent = (
|
|
309
|
-
<>
|
|
310
|
-
{Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
|
|
311
|
-
<ModernI18nProvider value={contextValue}>
|
|
312
|
-
{App ? <App {...props}>{children}</App> : children}
|
|
313
|
-
</ModernI18nProvider>
|
|
314
|
-
</>
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
if (!i18nInstance) {
|
|
318
|
-
return appContent;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (I18nextProvider) {
|
|
322
|
-
const i18nextInstanceForProvider =
|
|
323
|
-
getI18nextInstanceForProvider(i18nInstance);
|
|
324
|
-
return (
|
|
325
|
-
<I18nextProvider i18n={i18nextInstanceForProvider}>
|
|
326
|
-
{appContent}
|
|
327
|
-
</I18nextProvider>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return appContent;
|
|
332
|
-
};
|
|
333
|
-
});
|
|
90
|
+
api.wrapRoot(
|
|
91
|
+
createI18nRootWrapper({
|
|
92
|
+
entryName,
|
|
93
|
+
htmlLangAttr,
|
|
94
|
+
localePathRedirect,
|
|
95
|
+
languages,
|
|
96
|
+
fallbackLanguage,
|
|
97
|
+
ignoreRedirectRoutes,
|
|
98
|
+
localisedUrls,
|
|
99
|
+
getLatestI18nInstance: () => latestI18nInstance,
|
|
100
|
+
getI18nextProvider: () => I18nextProvider,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
334
103
|
},
|
|
335
104
|
});
|
|
336
105
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
I18nInitOptions,
|
|
3
|
+
I18nInstance,
|
|
4
|
+
LanguageDetectorOptions,
|
|
5
|
+
} from '../instance';
|
|
6
|
+
|
|
7
|
+
interface DetectorCacheEntry {
|
|
8
|
+
instance: I18nInstance;
|
|
9
|
+
isTemporary: boolean;
|
|
10
|
+
configKey: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const detectorInstanceCache = new WeakMap<
|
|
14
|
+
I18nInstance,
|
|
15
|
+
DetectorCacheEntry
|
|
16
|
+
>();
|
|
17
|
+
|
|
18
|
+
const DETECTOR_SAFE_OPTION_KEYS: string[] = [
|
|
19
|
+
'lowerCaseLng',
|
|
20
|
+
'nonExplicitSupportedLngs',
|
|
21
|
+
'load',
|
|
22
|
+
'partialBundledLanguages',
|
|
23
|
+
'returnNull',
|
|
24
|
+
'returnEmptyString',
|
|
25
|
+
'returnObjects',
|
|
26
|
+
'joinArrays',
|
|
27
|
+
'keySeparator',
|
|
28
|
+
'nsSeparator',
|
|
29
|
+
'pluralSeparator',
|
|
30
|
+
'contextSeparator',
|
|
31
|
+
'fallbackNS',
|
|
32
|
+
'ns',
|
|
33
|
+
'defaultNS',
|
|
34
|
+
'debug',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Stable stringify that sorts object keys to ensure consistent output
|
|
39
|
+
* regardless of property order
|
|
40
|
+
*/
|
|
41
|
+
const stableStringify = (value: any): string => {
|
|
42
|
+
if (value === null || value === undefined) {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof value !== 'object') {
|
|
47
|
+
return JSON.stringify(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
// Arrays maintain their order
|
|
52
|
+
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For objects, sort keys and recursively stringify values
|
|
56
|
+
const sortedKeys = Object.keys(value).sort();
|
|
57
|
+
const sortedEntries = sortedKeys.map(key => {
|
|
58
|
+
const stringifiedValue = stableStringify(value[key]);
|
|
59
|
+
return `${JSON.stringify(key)}:${stringifiedValue}`;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return `{${sortedEntries.join(',')}}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const buildDetectorConfigKey = (
|
|
66
|
+
languages: string[],
|
|
67
|
+
fallbackLanguage: string,
|
|
68
|
+
mergedDetection: LanguageDetectorOptions,
|
|
69
|
+
): string => {
|
|
70
|
+
return stableStringify({
|
|
71
|
+
languages,
|
|
72
|
+
fallbackLanguage,
|
|
73
|
+
detection: mergedDetection,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const pickSafeDetectionOptions = (
|
|
78
|
+
userInitOptions?: I18nInitOptions,
|
|
79
|
+
): Partial<I18nInitOptions> & Record<string, any> => {
|
|
80
|
+
if (!userInitOptions) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
const safeOptions: Partial<I18nInitOptions> & Record<string, any> = {};
|
|
84
|
+
for (const key of DETECTOR_SAFE_OPTION_KEYS) {
|
|
85
|
+
const value = (userInitOptions as any)[key];
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
safeOptions[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if ((userInitOptions as any).interpolation) {
|
|
91
|
+
safeOptions.interpolation = { ...(userInitOptions as any).interpolation };
|
|
92
|
+
}
|
|
93
|
+
return safeOptions;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cleanupDetectorCacheEntry = (entry?: DetectorCacheEntry) => {
|
|
97
|
+
if (!entry || !entry.isTemporary) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const instance = entry.instance as any;
|
|
101
|
+
try {
|
|
102
|
+
instance?.removeAllListeners?.();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
void error;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
instance?.off?.('*');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
void error;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
instance?.services?.backendConnector?.backend?.stop?.();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
void error;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
instance?.services?.backendConnector?.backend?.close?.();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
void error;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const createDetectorInstance = (
|
|
124
|
+
baseInstance: I18nInstance,
|
|
125
|
+
configKey: string,
|
|
126
|
+
): { instance: I18nInstance; isTemporary: boolean } => {
|
|
127
|
+
const cached = detectorInstanceCache.get(baseInstance);
|
|
128
|
+
if (cached && cached.configKey === configKey) {
|
|
129
|
+
return { instance: cached.instance, isTemporary: cached.isTemporary };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (cached) {
|
|
133
|
+
cleanupDetectorCacheEntry(cached);
|
|
134
|
+
detectorInstanceCache.delete(baseInstance);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const createNewInstance = (): {
|
|
138
|
+
instance: I18nInstance;
|
|
139
|
+
isTemporary: boolean;
|
|
140
|
+
} => {
|
|
141
|
+
if (typeof baseInstance.createInstance === 'function') {
|
|
142
|
+
try {
|
|
143
|
+
const created = baseInstance.createInstance();
|
|
144
|
+
if (created) {
|
|
145
|
+
return { instance: created, isTemporary: true };
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
void error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof baseInstance.cloneInstance === 'function') {
|
|
153
|
+
try {
|
|
154
|
+
const cloned = baseInstance.cloneInstance();
|
|
155
|
+
if (cloned) {
|
|
156
|
+
return { instance: cloned, isTemporary: true };
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
void error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { instance: baseInstance, isTemporary: false };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const created = createNewInstance();
|
|
167
|
+
if (created.isTemporary) {
|
|
168
|
+
detectorInstanceCache.set(baseInstance, {
|
|
169
|
+
instance: created.instance,
|
|
170
|
+
isTemporary: true,
|
|
171
|
+
configKey,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return created;
|
|
175
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { I18nInitOptions, I18nInstance } from '../instance';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildDetectorConfigKey,
|
|
6
|
+
createDetectorInstance,
|
|
7
|
+
detectorInstanceCache,
|
|
8
|
+
pickSafeDetectionOptions,
|
|
9
|
+
} from './cache';
|
|
10
|
+
|
|
11
|
+
import { mergeDetectionOptions } from './initOptions';
|
|
12
|
+
|
|
13
|
+
import { getSupportedLanguage, isLanguageSupported } from './language';
|
|
14
|
+
|
|
15
|
+
import { detectLanguage, useI18nextLanguageDetector } from './middleware';
|
|
16
|
+
|
|
17
|
+
import type { BaseLanguageDetectionOptions } from './types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize i18n instance for detector if needed
|
|
21
|
+
*/
|
|
22
|
+
interface DetectorInitResult {
|
|
23
|
+
detectorInstance: I18nInstance;
|
|
24
|
+
isTemporary: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initializeI18nForDetector = async (
|
|
28
|
+
i18nInstance: I18nInstance,
|
|
29
|
+
options: BaseLanguageDetectionOptions,
|
|
30
|
+
): Promise<DetectorInitResult> => {
|
|
31
|
+
const mergedDetection = mergeDetectionOptions(
|
|
32
|
+
options.i18nextDetector,
|
|
33
|
+
options.detection,
|
|
34
|
+
options.localePathRedirect,
|
|
35
|
+
options.userInitOptions,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const configKey = buildDetectorConfigKey(
|
|
39
|
+
options.languages,
|
|
40
|
+
options.fallbackLanguage,
|
|
41
|
+
mergedDetection,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const { instance, isTemporary } = createDetectorInstance(
|
|
45
|
+
i18nInstance,
|
|
46
|
+
configKey,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const safeUserOptions = pickSafeDetectionOptions(options.userInitOptions);
|
|
50
|
+
|
|
51
|
+
// Only initialize detection capability, don't load any resources to avoid conflicts with subsequent backend initialization
|
|
52
|
+
const initOptions: I18nInitOptions = {
|
|
53
|
+
...safeUserOptions,
|
|
54
|
+
fallbackLng: options.fallbackLanguage,
|
|
55
|
+
supportedLngs: options.languages,
|
|
56
|
+
detection: mergedDetection,
|
|
57
|
+
initImmediate: true,
|
|
58
|
+
interpolation: {
|
|
59
|
+
...(safeUserOptions?.interpolation || {}),
|
|
60
|
+
escapeValue: safeUserOptions?.interpolation?.escapeValue ?? false,
|
|
61
|
+
},
|
|
62
|
+
react: {
|
|
63
|
+
useSuspense: false,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Ensure the detector instance has the language detection plugin loaded
|
|
68
|
+
useI18nextLanguageDetector(instance);
|
|
69
|
+
|
|
70
|
+
if (!instance.isInitialized) {
|
|
71
|
+
await instance.init(initOptions);
|
|
72
|
+
} else if (isTemporary) {
|
|
73
|
+
await instance.init(initOptions);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { detectorInstance: instance, isTemporary };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Priority 3: Detect language using i18next detector
|
|
81
|
+
*/
|
|
82
|
+
export const detectLanguageFromI18nextDetector = async (
|
|
83
|
+
i18nInstance: I18nInstance,
|
|
84
|
+
options: BaseLanguageDetectionOptions & { ssrContext?: any },
|
|
85
|
+
): Promise<string | undefined> => {
|
|
86
|
+
if (!options.i18nextDetector) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Merge detection options to pass to detector
|
|
91
|
+
const mergedDetection = mergeDetectionOptions(
|
|
92
|
+
options.i18nextDetector,
|
|
93
|
+
options.detection,
|
|
94
|
+
options.localePathRedirect,
|
|
95
|
+
options.userInitOptions,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const { detectorInstance, isTemporary } = await initializeI18nForDetector(
|
|
99
|
+
i18nInstance,
|
|
100
|
+
options,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const request = options.ssrContext?.request;
|
|
105
|
+
if (!isBrowser() && !request) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const detectorLang = detectLanguage(
|
|
110
|
+
detectorInstance,
|
|
111
|
+
request as any,
|
|
112
|
+
mergedDetection,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Use getSupportedLanguage to get the matching supported language
|
|
116
|
+
// This handles both exact match and base language code match (e.g., 'zh-CN' -> 'zh')
|
|
117
|
+
if (detectorLang) {
|
|
118
|
+
const supportedLang = getSupportedLanguage(
|
|
119
|
+
detectorLang,
|
|
120
|
+
options.languages,
|
|
121
|
+
);
|
|
122
|
+
if (supportedLang) {
|
|
123
|
+
return supportedLang;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback to instance's current language if detector didn't detect
|
|
128
|
+
if (detectorInstance.isInitialized && detectorInstance.language) {
|
|
129
|
+
const currentLang = detectorInstance.language;
|
|
130
|
+
if (isLanguageSupported(currentLang, options.languages)) {
|
|
131
|
+
return currentLang;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Silently ignore errors
|
|
136
|
+
} finally {
|
|
137
|
+
// Clean up temporary instance to avoid affecting subsequent formal initialization
|
|
138
|
+
if (isTemporary && detectorInstance !== i18nInstance) {
|
|
139
|
+
// Temporary instance is saved in cache for reuse
|
|
140
|
+
detectorInstanceCache.set(i18nInstance, {
|
|
141
|
+
instance: detectorInstance,
|
|
142
|
+
isTemporary: true,
|
|
143
|
+
configKey: buildDetectorConfigKey(
|
|
144
|
+
options.languages,
|
|
145
|
+
options.fallbackLanguage,
|
|
146
|
+
mergedDetection,
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
} else if (detectorInstance === i18nInstance) {
|
|
150
|
+
// As a fallback, prevent i18nInstance from being polluted by detector init
|
|
151
|
+
(i18nInstance as any).isInitialized = false;
|
|
152
|
+
delete (i18nInstance as any).language;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
};
|