@bleedingdev/modern-js-plugin-i18n 3.4.0-ultramodern.1 → 3.4.0-ultramodern.10
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/context.js +8 -0
- package/dist/cjs/runtime/core.js +217 -0
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +22 -4
- package/dist/cjs/runtime/index.js +44 -173
- package/dist/cjs/runtime/no-react-i18next.js +76 -0
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/runtime/context.mjs +8 -0
- package/dist/esm/runtime/core.mjs +152 -0
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/esm/runtime/index.mjs +5 -140
- package/dist/esm/runtime/no-react-i18next.mjs +6 -0
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/runtime/context.mjs +8 -0
- package/dist/esm-node/runtime/core.mjs +153 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/esm-node/runtime/index.mjs +5 -140
- package/dist/esm-node/runtime/no-react-i18next.mjs +7 -0
- package/dist/types/runtime/context.d.ts +1 -0
- package/dist/types/runtime/core.d.ts +30 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +4 -1
- package/dist/types/runtime/index.d.ts +2 -24
- package/dist/types/runtime/no-react-i18next.d.ts +3 -0
- package/package.json +21 -11
- package/rstest.config.mts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/runtime/context.tsx +13 -0
- package/src/runtime/core.tsx +360 -0
- package/src/runtime/i18n/backend/middleware.node.ts +31 -1
- package/src/runtime/index.tsx +4 -316
- package/src/runtime/no-react-i18next.tsx +7 -0
- package/tests/i18nUtils.test.ts +34 -0
- package/tests/localisedUrls.test.ts +35 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +36 -0
- package/tests/routerAdapter.test.tsx +58 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
3
|
+
import { Helmet } from "@modern-js/runtime/head";
|
|
4
|
+
import { merge } from "@modern-js/runtime-utils/merge";
|
|
5
|
+
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { ModernI18nProvider, useModernI18n } from "./context.mjs";
|
|
7
|
+
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
8
|
+
import { getI18nInstance } from "./i18n/index.mjs";
|
|
9
|
+
import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
10
|
+
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
11
|
+
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
12
|
+
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
13
|
+
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
14
|
+
import "./types.mjs";
|
|
15
|
+
let i18nLifecycleHelpersPromise;
|
|
16
|
+
function loadI18nLifecycleHelpers() {
|
|
17
|
+
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
18
|
+
import("./i18n/backend/middleware.mjs"),
|
|
19
|
+
import("./i18n/utils.mjs")
|
|
20
|
+
]).then(([backendMiddleware, utils])=>({
|
|
21
|
+
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
22
|
+
changeI18nLanguage: utils.changeI18nLanguage,
|
|
23
|
+
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
24
|
+
initializeI18nInstance: utils.initializeI18nInstance,
|
|
25
|
+
setupClonedInstance: utils.setupClonedInstance
|
|
26
|
+
}));
|
|
27
|
+
return i18nLifecycleHelpersPromise;
|
|
28
|
+
}
|
|
29
|
+
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
30
|
+
name: '@modern-js/plugin-i18n',
|
|
31
|
+
setup: (api)=>{
|
|
32
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
33
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
|
|
34
|
+
const { enabled: backendEnabled = false } = backend || {};
|
|
35
|
+
let latestI18nInstance;
|
|
36
|
+
let I18nextProvider;
|
|
37
|
+
const resolveReactI18nextIntegration = async ()=>{
|
|
38
|
+
if (!reactI18next) return null;
|
|
39
|
+
return loadReactI18nextIntegration?.() ?? null;
|
|
40
|
+
};
|
|
41
|
+
api.onBeforeRender(async (context)=>{
|
|
42
|
+
const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
|
|
43
|
+
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
44
|
+
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
45
|
+
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
46
|
+
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
47
|
+
const reactI18nextIntegration = await resolveReactI18nextIntegration();
|
|
48
|
+
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
49
|
+
if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
50
|
+
const pathname = getPathname(context);
|
|
51
|
+
if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
|
|
52
|
+
const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
53
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
54
|
+
const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
|
|
55
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
|
|
56
|
+
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
57
|
+
languages,
|
|
58
|
+
fallbackLanguage,
|
|
59
|
+
localePathRedirect,
|
|
60
|
+
i18nextDetector,
|
|
61
|
+
detection,
|
|
62
|
+
userInitOptions,
|
|
63
|
+
mergedBackend,
|
|
64
|
+
pathname,
|
|
65
|
+
ssrContext: context.ssrContext
|
|
66
|
+
});
|
|
67
|
+
await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
|
|
68
|
+
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
69
|
+
i18nInstance = i18nInstance.cloneInstance();
|
|
70
|
+
await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
71
|
+
}
|
|
72
|
+
if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
73
|
+
if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
|
|
74
|
+
context.i18nInstance = i18nInstance;
|
|
75
|
+
latestI18nInstance = i18nInstance;
|
|
76
|
+
context.changeLanguage = async (newLang)=>{
|
|
77
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
78
|
+
detectionOptions: mergedDetection
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
api.wrapRoot((App)=>(props)=>{
|
|
83
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
84
|
+
const i18nInstance = runtimeContext.i18nInstance || latestI18nInstance;
|
|
85
|
+
const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
|
|
86
|
+
i18nInstance?.language,
|
|
87
|
+
localeDetection?.fallbackLanguage
|
|
88
|
+
]);
|
|
89
|
+
const [lang, setLang] = useState(initialLang);
|
|
90
|
+
const [forceUpdate, setForceUpdate] = useState(0);
|
|
91
|
+
const prevLangRef = useRef(lang);
|
|
92
|
+
const runtimeContextRef = useRef(runtimeContext);
|
|
93
|
+
runtimeContextRef.current = runtimeContext;
|
|
94
|
+
useEffect(()=>{
|
|
95
|
+
if (i18nInstance?.language) {
|
|
96
|
+
const translator = i18nInstance.translator;
|
|
97
|
+
if (translator) translator.language = i18nInstance.language;
|
|
98
|
+
}
|
|
99
|
+
}, [
|
|
100
|
+
i18nInstance?.language
|
|
101
|
+
]);
|
|
102
|
+
useEffect(()=>{
|
|
103
|
+
prevLangRef.current = lang;
|
|
104
|
+
}, [
|
|
105
|
+
lang
|
|
106
|
+
]);
|
|
107
|
+
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
108
|
+
useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
|
|
109
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
|
|
110
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
|
|
111
|
+
lang,
|
|
112
|
+
i18nInstance,
|
|
113
|
+
entryName,
|
|
114
|
+
languages,
|
|
115
|
+
localePathRedirect,
|
|
116
|
+
ignoreRedirectRoutes,
|
|
117
|
+
localisedUrls,
|
|
118
|
+
forceUpdate
|
|
119
|
+
]);
|
|
120
|
+
const children = props.children;
|
|
121
|
+
const appContent = /*#__PURE__*/ jsxs(Fragment, {
|
|
122
|
+
children: [
|
|
123
|
+
Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
|
|
124
|
+
htmlAttributes: {
|
|
125
|
+
lang
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
129
|
+
value: contextValue,
|
|
130
|
+
children: App ? /*#__PURE__*/ jsx(App, {
|
|
131
|
+
...props,
|
|
132
|
+
children: children
|
|
133
|
+
}) : children
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
});
|
|
137
|
+
if (!i18nInstance) return appContent;
|
|
138
|
+
if (I18nextProvider) {
|
|
139
|
+
const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
|
|
140
|
+
return /*#__PURE__*/ jsx(I18nextProvider, {
|
|
141
|
+
i18n: i18nextInstanceForProvider,
|
|
142
|
+
children: appContent
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return appContent;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
export { I18nLink } from "./I18nLink.mjs";
|
|
150
|
+
export { Link } from "./Link.mjs";
|
|
151
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
152
|
+
export { buildLocalizedUrl, createI18nPlugin, splitUrlTarget, useModernI18n };
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import i18next_fs_backend from "i18next-fs-backend";
|
|
2
2
|
import { useI18nextBackendCommon } from "./middleware.common.mjs";
|
|
3
|
-
|
|
3
|
+
const resolveFsBackendConstructor = (backendModule)=>{
|
|
4
|
+
const nestedDefault = backendModule?.default?.default;
|
|
5
|
+
const nestedModuleExports = backendModule?.default?.['module.exports'];
|
|
6
|
+
const candidates = [
|
|
7
|
+
backendModule,
|
|
8
|
+
backendModule?.default,
|
|
9
|
+
backendModule?.['module.exports'],
|
|
10
|
+
nestedDefault,
|
|
11
|
+
nestedModuleExports
|
|
12
|
+
];
|
|
13
|
+
const Backend = candidates.find((candidate)=>'function' == typeof candidate);
|
|
14
|
+
if (!Backend) throw new Error('Failed to resolve i18next-fs-backend constructor for the i18n Node backend.');
|
|
15
|
+
return Backend;
|
|
16
|
+
};
|
|
17
|
+
const middleware_node_Backend = resolveFsBackendConstructor(i18next_fs_backend);
|
|
18
|
+
class FsBackendWithSave extends middleware_node_Backend {
|
|
4
19
|
save(_language, _namespace, _data) {}
|
|
5
20
|
}
|
|
6
21
|
const HttpBackendWithSave = FsBackendWithSave;
|
|
7
|
-
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave,
|
|
8
|
-
export { FsBackendWithSave, HttpBackendWithSave, useI18nextBackend };
|
|
22
|
+
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, middleware_node_Backend, backend);
|
|
23
|
+
export { FsBackendWithSave, HttpBackendWithSave, resolveFsBackendConstructor, useI18nextBackend };
|
|
@@ -1,142 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import { ModernI18nProvider, useModernI18n } from "./context.mjs";
|
|
7
|
-
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
8
|
-
import { getI18nInstance } from "./i18n/index.mjs";
|
|
9
|
-
import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
10
|
-
import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
|
|
11
|
-
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
12
|
-
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
13
|
-
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
14
|
-
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
15
|
-
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
16
|
-
import "./types.mjs";
|
|
17
|
-
const i18nPlugin = (options)=>({
|
|
18
|
-
name: '@modern-js/plugin-i18n',
|
|
19
|
-
setup: (api)=>{
|
|
20
|
-
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
21
|
-
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
|
|
22
|
-
const { enabled: backendEnabled = false } = backend || {};
|
|
23
|
-
let latestI18nInstance;
|
|
24
|
-
let I18nextProvider;
|
|
25
|
-
const loadReactI18nextIntegration = async ()=>{
|
|
26
|
-
if (!reactI18next) return null;
|
|
27
|
-
const { getReactI18nextIntegration } = await import("./i18n/react-i18next.mjs");
|
|
28
|
-
return getReactI18nextIntegration();
|
|
29
|
-
};
|
|
30
|
-
api.onBeforeRender(async (context)=>{
|
|
31
|
-
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
32
|
-
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
33
|
-
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
34
|
-
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
35
|
-
const reactI18nextIntegration = await loadReactI18nextIntegration();
|
|
36
|
-
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
37
|
-
if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
38
|
-
const pathname = getPathname(context);
|
|
39
|
-
if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
|
|
40
|
-
const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
41
|
-
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
42
|
-
const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
|
|
43
|
-
if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
|
|
44
|
-
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
45
|
-
languages,
|
|
46
|
-
fallbackLanguage,
|
|
47
|
-
localePathRedirect,
|
|
48
|
-
i18nextDetector,
|
|
49
|
-
detection,
|
|
50
|
-
userInitOptions,
|
|
51
|
-
mergedBackend,
|
|
52
|
-
pathname,
|
|
53
|
-
ssrContext: context.ssrContext
|
|
54
|
-
});
|
|
55
|
-
await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
|
|
56
|
-
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
57
|
-
i18nInstance = i18nInstance.cloneInstance();
|
|
58
|
-
await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
59
|
-
}
|
|
60
|
-
if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
61
|
-
if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
|
|
62
|
-
context.i18nInstance = i18nInstance;
|
|
63
|
-
latestI18nInstance = i18nInstance;
|
|
64
|
-
context.changeLanguage = async (newLang)=>{
|
|
65
|
-
await changeI18nLanguage(i18nInstance, newLang, {
|
|
66
|
-
detectionOptions: mergedDetection
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
api.wrapRoot((App)=>(props)=>{
|
|
71
|
-
const runtimeContext = useContext(RuntimeContext);
|
|
72
|
-
const i18nInstance = runtimeContext.i18nInstance || latestI18nInstance;
|
|
73
|
-
const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
|
|
74
|
-
i18nInstance?.language,
|
|
75
|
-
localeDetection?.fallbackLanguage
|
|
76
|
-
]);
|
|
77
|
-
const [lang, setLang] = useState(initialLang);
|
|
78
|
-
const [forceUpdate, setForceUpdate] = useState(0);
|
|
79
|
-
const prevLangRef = useRef(lang);
|
|
80
|
-
const runtimeContextRef = useRef(runtimeContext);
|
|
81
|
-
runtimeContextRef.current = runtimeContext;
|
|
82
|
-
useEffect(()=>{
|
|
83
|
-
if (i18nInstance?.language) {
|
|
84
|
-
const translator = i18nInstance.translator;
|
|
85
|
-
if (translator) translator.language = i18nInstance.language;
|
|
86
|
-
}
|
|
87
|
-
}, [
|
|
88
|
-
i18nInstance?.language
|
|
89
|
-
]);
|
|
90
|
-
useEffect(()=>{
|
|
91
|
-
prevLangRef.current = lang;
|
|
92
|
-
}, [
|
|
93
|
-
lang
|
|
94
|
-
]);
|
|
95
|
-
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
96
|
-
useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
|
|
97
|
-
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
|
|
98
|
-
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
|
|
99
|
-
lang,
|
|
100
|
-
i18nInstance,
|
|
101
|
-
entryName,
|
|
102
|
-
languages,
|
|
103
|
-
localePathRedirect,
|
|
104
|
-
ignoreRedirectRoutes,
|
|
105
|
-
localisedUrls,
|
|
106
|
-
forceUpdate
|
|
107
|
-
]);
|
|
108
|
-
const children = props.children;
|
|
109
|
-
const appContent = /*#__PURE__*/ jsxs(Fragment, {
|
|
110
|
-
children: [
|
|
111
|
-
Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
|
|
112
|
-
htmlAttributes: {
|
|
113
|
-
lang
|
|
114
|
-
}
|
|
115
|
-
}),
|
|
116
|
-
/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
117
|
-
value: contextValue,
|
|
118
|
-
children: App ? /*#__PURE__*/ jsx(App, {
|
|
119
|
-
...props,
|
|
120
|
-
children: children
|
|
121
|
-
}) : children
|
|
122
|
-
})
|
|
123
|
-
]
|
|
124
|
-
});
|
|
125
|
-
if (!i18nInstance) return appContent;
|
|
126
|
-
if (I18nextProvider) {
|
|
127
|
-
const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
|
|
128
|
-
return /*#__PURE__*/ jsx(I18nextProvider, {
|
|
129
|
-
i18n: i18nextInstanceForProvider,
|
|
130
|
-
children: appContent
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return appContent;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
});
|
|
1
|
+
import { createI18nPlugin } from "./core.mjs";
|
|
2
|
+
import { getReactI18nextIntegration } from "./i18n/react-i18next.mjs";
|
|
3
|
+
export * from "./core.mjs";
|
|
4
|
+
const i18nPlugin = createI18nPlugin(getReactI18nextIntegration);
|
|
137
5
|
const runtime = i18nPlugin;
|
|
138
|
-
export { I18nLink } from "./I18nLink.mjs";
|
|
139
|
-
export { Link } from "./Link.mjs";
|
|
140
|
-
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
141
6
|
export default runtime;
|
|
142
|
-
export {
|
|
7
|
+
export { i18nPlugin };
|
|
@@ -32,9 +32,10 @@ const i18nPlugin = (options = {})=>({
|
|
|
32
32
|
backend: backendOptions,
|
|
33
33
|
...extendedConfig
|
|
34
34
|
};
|
|
35
|
+
const runtimePluginPath = customPlugin?.runtime?.path || (false === config.reactI18next ? `@${metaName}/plugin-i18n/runtime/no-react-i18next` : `@${metaName}/plugin-i18n/runtime`);
|
|
35
36
|
plugins.push({
|
|
36
37
|
name: customPlugin?.runtime?.name || 'i18n',
|
|
37
|
-
path:
|
|
38
|
+
path: runtimePluginPath,
|
|
38
39
|
config
|
|
39
40
|
});
|
|
40
41
|
return {
|
|
@@ -90,6 +90,13 @@ const useModernI18n = ()=>{
|
|
|
90
90
|
navigate,
|
|
91
91
|
location
|
|
92
92
|
]);
|
|
93
|
+
const t = useCallback((key, ...args)=>{
|
|
94
|
+
if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
|
|
95
|
+
return i18nInstance.t(key, ...args);
|
|
96
|
+
}, [
|
|
97
|
+
currentLanguage,
|
|
98
|
+
i18nInstance
|
|
99
|
+
]);
|
|
93
100
|
const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
|
|
94
101
|
languages
|
|
95
102
|
]);
|
|
@@ -123,6 +130,7 @@ const useModernI18n = ()=>{
|
|
|
123
130
|
return {
|
|
124
131
|
language: currentLanguage,
|
|
125
132
|
changeLanguage,
|
|
133
|
+
t,
|
|
126
134
|
i18nInstance,
|
|
127
135
|
supportedLanguages: languages || [],
|
|
128
136
|
localisedUrls,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
4
|
+
import { Helmet } from "@modern-js/runtime/head";
|
|
5
|
+
import { merge } from "@modern-js/runtime-utils/merge";
|
|
6
|
+
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { ModernI18nProvider, useModernI18n } from "./context.mjs";
|
|
8
|
+
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
9
|
+
import { getI18nInstance } from "./i18n/index.mjs";
|
|
10
|
+
import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
11
|
+
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
12
|
+
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
13
|
+
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
14
|
+
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
15
|
+
import "./types.mjs";
|
|
16
|
+
let i18nLifecycleHelpersPromise;
|
|
17
|
+
function loadI18nLifecycleHelpers() {
|
|
18
|
+
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
19
|
+
import("./i18n/backend/middleware.mjs"),
|
|
20
|
+
import("./i18n/utils.mjs")
|
|
21
|
+
]).then(([backendMiddleware, utils])=>({
|
|
22
|
+
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
23
|
+
changeI18nLanguage: utils.changeI18nLanguage,
|
|
24
|
+
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
25
|
+
initializeI18nInstance: utils.initializeI18nInstance,
|
|
26
|
+
setupClonedInstance: utils.setupClonedInstance
|
|
27
|
+
}));
|
|
28
|
+
return i18nLifecycleHelpersPromise;
|
|
29
|
+
}
|
|
30
|
+
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
31
|
+
name: '@modern-js/plugin-i18n',
|
|
32
|
+
setup: (api)=>{
|
|
33
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
34
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
|
|
35
|
+
const { enabled: backendEnabled = false } = backend || {};
|
|
36
|
+
let latestI18nInstance;
|
|
37
|
+
let I18nextProvider;
|
|
38
|
+
const resolveReactI18nextIntegration = async ()=>{
|
|
39
|
+
if (!reactI18next) return null;
|
|
40
|
+
return loadReactI18nextIntegration?.() ?? null;
|
|
41
|
+
};
|
|
42
|
+
api.onBeforeRender(async (context)=>{
|
|
43
|
+
const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
|
|
44
|
+
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
45
|
+
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
46
|
+
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
47
|
+
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
48
|
+
const reactI18nextIntegration = await resolveReactI18nextIntegration();
|
|
49
|
+
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
50
|
+
if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
51
|
+
const pathname = getPathname(context);
|
|
52
|
+
if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
|
|
53
|
+
const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
54
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
55
|
+
const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
|
|
56
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
|
|
57
|
+
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
58
|
+
languages,
|
|
59
|
+
fallbackLanguage,
|
|
60
|
+
localePathRedirect,
|
|
61
|
+
i18nextDetector,
|
|
62
|
+
detection,
|
|
63
|
+
userInitOptions,
|
|
64
|
+
mergedBackend,
|
|
65
|
+
pathname,
|
|
66
|
+
ssrContext: context.ssrContext
|
|
67
|
+
});
|
|
68
|
+
await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
|
|
69
|
+
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
70
|
+
i18nInstance = i18nInstance.cloneInstance();
|
|
71
|
+
await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
72
|
+
}
|
|
73
|
+
if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
74
|
+
if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
|
|
75
|
+
context.i18nInstance = i18nInstance;
|
|
76
|
+
latestI18nInstance = i18nInstance;
|
|
77
|
+
context.changeLanguage = async (newLang)=>{
|
|
78
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
79
|
+
detectionOptions: mergedDetection
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
api.wrapRoot((App)=>(props)=>{
|
|
84
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
85
|
+
const i18nInstance = runtimeContext.i18nInstance || latestI18nInstance;
|
|
86
|
+
const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
|
|
87
|
+
i18nInstance?.language,
|
|
88
|
+
localeDetection?.fallbackLanguage
|
|
89
|
+
]);
|
|
90
|
+
const [lang, setLang] = useState(initialLang);
|
|
91
|
+
const [forceUpdate, setForceUpdate] = useState(0);
|
|
92
|
+
const prevLangRef = useRef(lang);
|
|
93
|
+
const runtimeContextRef = useRef(runtimeContext);
|
|
94
|
+
runtimeContextRef.current = runtimeContext;
|
|
95
|
+
useEffect(()=>{
|
|
96
|
+
if (i18nInstance?.language) {
|
|
97
|
+
const translator = i18nInstance.translator;
|
|
98
|
+
if (translator) translator.language = i18nInstance.language;
|
|
99
|
+
}
|
|
100
|
+
}, [
|
|
101
|
+
i18nInstance?.language
|
|
102
|
+
]);
|
|
103
|
+
useEffect(()=>{
|
|
104
|
+
prevLangRef.current = lang;
|
|
105
|
+
}, [
|
|
106
|
+
lang
|
|
107
|
+
]);
|
|
108
|
+
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
109
|
+
useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
|
|
110
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
|
|
111
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
|
|
112
|
+
lang,
|
|
113
|
+
i18nInstance,
|
|
114
|
+
entryName,
|
|
115
|
+
languages,
|
|
116
|
+
localePathRedirect,
|
|
117
|
+
ignoreRedirectRoutes,
|
|
118
|
+
localisedUrls,
|
|
119
|
+
forceUpdate
|
|
120
|
+
]);
|
|
121
|
+
const children = props.children;
|
|
122
|
+
const appContent = /*#__PURE__*/ jsxs(Fragment, {
|
|
123
|
+
children: [
|
|
124
|
+
Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
|
|
125
|
+
htmlAttributes: {
|
|
126
|
+
lang
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
130
|
+
value: contextValue,
|
|
131
|
+
children: App ? /*#__PURE__*/ jsx(App, {
|
|
132
|
+
...props,
|
|
133
|
+
children: children
|
|
134
|
+
}) : children
|
|
135
|
+
})
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
if (!i18nInstance) return appContent;
|
|
139
|
+
if (I18nextProvider) {
|
|
140
|
+
const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
|
|
141
|
+
return /*#__PURE__*/ jsx(I18nextProvider, {
|
|
142
|
+
i18n: i18nextInstanceForProvider,
|
|
143
|
+
children: appContent
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return appContent;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
export { I18nLink } from "./I18nLink.mjs";
|
|
151
|
+
export { Link } from "./Link.mjs";
|
|
152
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
153
|
+
export { buildLocalizedUrl, createI18nPlugin, splitUrlTarget, useModernI18n };
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import
|
|
2
|
+
import i18next_fs_backend from "i18next-fs-backend";
|
|
3
3
|
import { useI18nextBackendCommon } from "./middleware.common.mjs";
|
|
4
|
-
|
|
4
|
+
const resolveFsBackendConstructor = (backendModule)=>{
|
|
5
|
+
const nestedDefault = backendModule?.default?.default;
|
|
6
|
+
const nestedModuleExports = backendModule?.default?.['module.exports'];
|
|
7
|
+
const candidates = [
|
|
8
|
+
backendModule,
|
|
9
|
+
backendModule?.default,
|
|
10
|
+
backendModule?.['module.exports'],
|
|
11
|
+
nestedDefault,
|
|
12
|
+
nestedModuleExports
|
|
13
|
+
];
|
|
14
|
+
const Backend = candidates.find((candidate)=>'function' == typeof candidate);
|
|
15
|
+
if (!Backend) throw new Error('Failed to resolve i18next-fs-backend constructor for the i18n Node backend.');
|
|
16
|
+
return Backend;
|
|
17
|
+
};
|
|
18
|
+
const middleware_node_Backend = resolveFsBackendConstructor(i18next_fs_backend);
|
|
19
|
+
class FsBackendWithSave extends middleware_node_Backend {
|
|
5
20
|
save(_language, _namespace, _data) {}
|
|
6
21
|
}
|
|
7
22
|
const HttpBackendWithSave = FsBackendWithSave;
|
|
8
|
-
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave,
|
|
9
|
-
export { FsBackendWithSave, HttpBackendWithSave, useI18nextBackend };
|
|
23
|
+
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, middleware_node_Backend, backend);
|
|
24
|
+
export { FsBackendWithSave, HttpBackendWithSave, resolveFsBackendConstructor, useI18nextBackend };
|