@bleedingdev/modern-js-plugin-i18n 3.4.0-ultramodern.2 → 3.4.0-ultramodern.20
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/runtime/context.js +15 -1
- package/dist/cjs/runtime/core.js +22 -9
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +22 -4
- package/dist/esm/runtime/context.mjs +15 -1
- package/dist/esm/runtime/core.mjs +15 -2
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/esm-node/runtime/context.mjs +15 -1
- package/dist/esm-node/runtime/core.mjs +15 -2
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/types/runtime/context.d.ts +1 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +4 -1
- package/package.json +10 -10
- package/src/runtime/context.tsx +31 -1
- package/src/runtime/core.tsx +32 -7
- package/src/runtime/i18n/backend/middleware.node.ts +31 -1
- package/tests/i18nUtils.test.ts +34 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +18 -0
- package/tests/routerAdapter.test.tsx +58 -1
|
@@ -37,7 +37,13 @@ const external_react_namespaceObject = require("react");
|
|
|
37
37
|
const index_js_namespaceObject = require("./i18n/detection/index.js");
|
|
38
38
|
const external_routerAdapter_js_namespaceObject = require("./routerAdapter.js");
|
|
39
39
|
const external_utils_js_namespaceObject = require("./utils.js");
|
|
40
|
-
const
|
|
40
|
+
const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
|
|
41
|
+
const getModernI18nContext = ()=>{
|
|
42
|
+
const globalStore = globalThis;
|
|
43
|
+
globalStore[modernI18nContextKey] ??= /*#__PURE__*/ (0, external_react_namespaceObject.createContext)(null);
|
|
44
|
+
return globalStore[modernI18nContextKey];
|
|
45
|
+
};
|
|
46
|
+
const ModernI18nContext = getModernI18nContext();
|
|
41
47
|
const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(ModernI18nContext.Provider, {
|
|
42
48
|
value: value,
|
|
43
49
|
children: children
|
|
@@ -122,6 +128,13 @@ const useModernI18n = ()=>{
|
|
|
122
128
|
navigate,
|
|
123
129
|
location
|
|
124
130
|
]);
|
|
131
|
+
const t = (0, external_react_namespaceObject.useCallback)((key, ...args)=>{
|
|
132
|
+
if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
|
|
133
|
+
return i18nInstance.t(key, ...args);
|
|
134
|
+
}, [
|
|
135
|
+
currentLanguage,
|
|
136
|
+
i18nInstance
|
|
137
|
+
]);
|
|
125
138
|
const isLanguageSupported = (0, external_react_namespaceObject.useCallback)((lang)=>languages?.includes(lang) || false, [
|
|
126
139
|
languages
|
|
127
140
|
]);
|
|
@@ -155,6 +168,7 @@ const useModernI18n = ()=>{
|
|
|
155
168
|
return {
|
|
156
169
|
language: currentLanguage,
|
|
157
170
|
changeLanguage,
|
|
171
|
+
t,
|
|
158
172
|
i18nInstance,
|
|
159
173
|
supportedLanguages: languages || [],
|
|
160
174
|
localisedUrls,
|
package/dist/cjs/runtime/core.js
CHANGED
|
@@ -48,16 +48,28 @@ const external_context_js_namespaceObject = require("./context.js");
|
|
|
48
48
|
const external_hooks_js_namespaceObject = require("./hooks.js");
|
|
49
49
|
const index_js_namespaceObject = require("./i18n/index.js");
|
|
50
50
|
const backend_index_js_namespaceObject = require("./i18n/backend/index.js");
|
|
51
|
-
const middleware_js_namespaceObject = require("./i18n/backend/middleware.js");
|
|
52
51
|
const detection_index_js_namespaceObject = require("./i18n/detection/index.js");
|
|
53
|
-
const
|
|
52
|
+
const middleware_js_namespaceObject = require("./i18n/detection/middleware.js");
|
|
54
53
|
const instance_js_namespaceObject = require("./i18n/instance.js");
|
|
55
|
-
const utils_js_namespaceObject = require("./i18n/utils.js");
|
|
56
54
|
const external_utils_js_namespaceObject = require("./utils.js");
|
|
57
55
|
require("./types.js");
|
|
58
56
|
const external_I18nLink_js_namespaceObject = require("./I18nLink.js");
|
|
59
57
|
const external_Link_js_namespaceObject = require("./Link.js");
|
|
60
58
|
const external_localizedPaths_js_namespaceObject = require("./localizedPaths.js");
|
|
59
|
+
let i18nLifecycleHelpersPromise;
|
|
60
|
+
function loadI18nLifecycleHelpers() {
|
|
61
|
+
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
62
|
+
import("./i18n/backend/middleware.js"),
|
|
63
|
+
import("./i18n/utils.js")
|
|
64
|
+
]).then(([backendMiddleware, utils])=>({
|
|
65
|
+
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
66
|
+
changeI18nLanguage: utils.changeI18nLanguage,
|
|
67
|
+
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
68
|
+
initializeI18nInstance: utils.initializeI18nInstance,
|
|
69
|
+
setupClonedInstance: utils.setupClonedInstance
|
|
70
|
+
}));
|
|
71
|
+
return i18nLifecycleHelpersPromise;
|
|
72
|
+
}
|
|
61
73
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
62
74
|
name: '@modern-js/plugin-i18n',
|
|
63
75
|
setup: (api)=>{
|
|
@@ -71,6 +83,7 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
71
83
|
return loadReactI18nextIntegration?.() ?? null;
|
|
72
84
|
};
|
|
73
85
|
api.onBeforeRender(async (context)=>{
|
|
86
|
+
const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
|
|
74
87
|
let i18nInstance = await (0, index_js_namespaceObject.getI18nInstance)(userI18nInstance);
|
|
75
88
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
76
89
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
@@ -79,11 +92,11 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
79
92
|
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
80
93
|
if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
81
94
|
const pathname = (0, external_utils_js_namespaceObject.getPathname)(context);
|
|
82
|
-
if (i18nextDetector) (0,
|
|
95
|
+
if (i18nextDetector) (0, middleware_js_namespaceObject.useI18nextLanguageDetector)(i18nInstance);
|
|
83
96
|
const mergedDetection = (0, detection_index_js_namespaceObject.mergeDetectionOptions)(i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
84
97
|
const mergedBackend = (0, backend_index_js_namespaceObject.mergeBackendOptions)(backend, userInitOptions);
|
|
85
98
|
const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
|
|
86
|
-
if (mergedBackend && (backendEnabled || hasSdkConfig))
|
|
99
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
|
|
87
100
|
const { finalLanguage } = await (0, detection_index_js_namespaceObject.detectLanguageWithPriority)(i18nInstance, {
|
|
88
101
|
languages,
|
|
89
102
|
fallbackLanguage,
|
|
@@ -95,17 +108,17 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
95
108
|
pathname,
|
|
96
109
|
ssrContext: context.ssrContext
|
|
97
110
|
});
|
|
98
|
-
await
|
|
111
|
+
await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
|
|
99
112
|
if (!(0, runtime_namespaceObject.isBrowser)() && i18nInstance.cloneInstance) {
|
|
100
113
|
i18nInstance = i18nInstance.cloneInstance();
|
|
101
|
-
await
|
|
114
|
+
await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
102
115
|
}
|
|
103
|
-
if (localePathRedirect) await
|
|
116
|
+
if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
104
117
|
if (!(0, runtime_namespaceObject.isBrowser)()) (0, detection_index_js_namespaceObject.exportServerLngToWindow)(context, finalLanguage);
|
|
105
118
|
context.i18nInstance = i18nInstance;
|
|
106
119
|
latestI18nInstance = i18nInstance;
|
|
107
120
|
context.changeLanguage = async (newLang)=>{
|
|
108
|
-
await
|
|
121
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
109
122
|
detectionOptions: mergedDetection
|
|
110
123
|
});
|
|
111
124
|
};
|
|
@@ -39,22 +39,40 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
39
39
|
__webpack_require__.d(__webpack_exports__, {
|
|
40
40
|
FsBackendWithSave: ()=>FsBackendWithSave,
|
|
41
41
|
HttpBackendWithSave: ()=>HttpBackendWithSave,
|
|
42
|
+
resolveFsBackendConstructor: ()=>resolveFsBackendConstructor,
|
|
42
43
|
useI18nextBackend: ()=>useI18nextBackend
|
|
43
44
|
});
|
|
44
|
-
const
|
|
45
|
-
var
|
|
45
|
+
const external_i18next_fs_backend_namespaceObject = require("i18next-fs-backend");
|
|
46
|
+
var external_i18next_fs_backend_default = /*#__PURE__*/ __webpack_require__.n(external_i18next_fs_backend_namespaceObject);
|
|
46
47
|
const external_middleware_common_js_namespaceObject = require("./middleware.common.js");
|
|
47
|
-
|
|
48
|
+
const resolveFsBackendConstructor = (backendModule)=>{
|
|
49
|
+
const nestedDefault = backendModule?.default?.default;
|
|
50
|
+
const nestedModuleExports = backendModule?.default?.['module.exports'];
|
|
51
|
+
const candidates = [
|
|
52
|
+
backendModule,
|
|
53
|
+
backendModule?.default,
|
|
54
|
+
backendModule?.['module.exports'],
|
|
55
|
+
nestedDefault,
|
|
56
|
+
nestedModuleExports
|
|
57
|
+
];
|
|
58
|
+
const Backend = candidates.find((candidate)=>'function' == typeof candidate);
|
|
59
|
+
if (!Backend) throw new Error('Failed to resolve i18next-fs-backend constructor for the i18n Node backend.');
|
|
60
|
+
return Backend;
|
|
61
|
+
};
|
|
62
|
+
const middleware_node_Backend = resolveFsBackendConstructor(external_i18next_fs_backend_default());
|
|
63
|
+
class FsBackendWithSave extends middleware_node_Backend {
|
|
48
64
|
save(_language, _namespace, _data) {}
|
|
49
65
|
}
|
|
50
66
|
const HttpBackendWithSave = FsBackendWithSave;
|
|
51
|
-
const useI18nextBackend = (i18nInstance, backend)=>(0, external_middleware_common_js_namespaceObject.useI18nextBackendCommon)(i18nInstance, FsBackendWithSave,
|
|
67
|
+
const useI18nextBackend = (i18nInstance, backend)=>(0, external_middleware_common_js_namespaceObject.useI18nextBackendCommon)(i18nInstance, FsBackendWithSave, middleware_node_Backend, backend);
|
|
52
68
|
exports.FsBackendWithSave = __webpack_exports__.FsBackendWithSave;
|
|
53
69
|
exports.HttpBackendWithSave = __webpack_exports__.HttpBackendWithSave;
|
|
70
|
+
exports.resolveFsBackendConstructor = __webpack_exports__.resolveFsBackendConstructor;
|
|
54
71
|
exports.useI18nextBackend = __webpack_exports__.useI18nextBackend;
|
|
55
72
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
56
73
|
"FsBackendWithSave",
|
|
57
74
|
"HttpBackendWithSave",
|
|
75
|
+
"resolveFsBackendConstructor",
|
|
58
76
|
"useI18nextBackend"
|
|
59
77
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
60
78
|
Object.defineProperty(exports, '__esModule', {
|
|
@@ -4,7 +4,13 @@ import { createContext, useCallback, useContext, useEffect, useMemo } from "reac
|
|
|
4
4
|
import { cacheUserLanguage } from "./i18n/detection/index.mjs";
|
|
5
5
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
6
|
import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
|
|
7
|
-
const
|
|
7
|
+
const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
|
|
8
|
+
const getModernI18nContext = ()=>{
|
|
9
|
+
const globalStore = globalThis;
|
|
10
|
+
globalStore[modernI18nContextKey] ??= /*#__PURE__*/ createContext(null);
|
|
11
|
+
return globalStore[modernI18nContextKey];
|
|
12
|
+
};
|
|
13
|
+
const ModernI18nContext = getModernI18nContext();
|
|
8
14
|
const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nContext.Provider, {
|
|
9
15
|
value: value,
|
|
10
16
|
children: children
|
|
@@ -89,6 +95,13 @@ const useModernI18n = ()=>{
|
|
|
89
95
|
navigate,
|
|
90
96
|
location
|
|
91
97
|
]);
|
|
98
|
+
const t = useCallback((key, ...args)=>{
|
|
99
|
+
if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
|
|
100
|
+
return i18nInstance.t(key, ...args);
|
|
101
|
+
}, [
|
|
102
|
+
currentLanguage,
|
|
103
|
+
i18nInstance
|
|
104
|
+
]);
|
|
92
105
|
const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
|
|
93
106
|
languages
|
|
94
107
|
]);
|
|
@@ -122,6 +135,7 @@ const useModernI18n = ()=>{
|
|
|
122
135
|
return {
|
|
123
136
|
language: currentLanguage,
|
|
124
137
|
changeLanguage,
|
|
138
|
+
t,
|
|
125
139
|
i18nInstance,
|
|
126
140
|
supportedLanguages: languages || [],
|
|
127
141
|
localisedUrls,
|
|
@@ -7,13 +7,25 @@ import { ModernI18nProvider, useModernI18n } from "./context.mjs";
|
|
|
7
7
|
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
8
8
|
import { getI18nInstance } from "./i18n/index.mjs";
|
|
9
9
|
import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
10
|
-
import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
|
|
11
10
|
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
12
11
|
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
13
12
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
14
|
-
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
15
13
|
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
16
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
|
+
}
|
|
17
29
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
18
30
|
name: '@modern-js/plugin-i18n',
|
|
19
31
|
setup: (api)=>{
|
|
@@ -27,6 +39,7 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
27
39
|
return loadReactI18nextIntegration?.() ?? null;
|
|
28
40
|
};
|
|
29
41
|
api.onBeforeRender(async (context)=>{
|
|
42
|
+
const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
|
|
30
43
|
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
31
44
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
32
45
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
@@ -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 };
|
|
@@ -5,7 +5,13 @@ import { createContext, useCallback, useContext, useEffect, useMemo } from "reac
|
|
|
5
5
|
import { cacheUserLanguage } from "./i18n/detection/index.mjs";
|
|
6
6
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
7
|
import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
|
|
8
|
-
const
|
|
8
|
+
const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
|
|
9
|
+
const getModernI18nContext = ()=>{
|
|
10
|
+
const globalStore = globalThis;
|
|
11
|
+
globalStore[modernI18nContextKey] ??= /*#__PURE__*/ createContext(null);
|
|
12
|
+
return globalStore[modernI18nContextKey];
|
|
13
|
+
};
|
|
14
|
+
const ModernI18nContext = getModernI18nContext();
|
|
9
15
|
const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nContext.Provider, {
|
|
10
16
|
value: value,
|
|
11
17
|
children: children
|
|
@@ -90,6 +96,13 @@ const useModernI18n = ()=>{
|
|
|
90
96
|
navigate,
|
|
91
97
|
location
|
|
92
98
|
]);
|
|
99
|
+
const t = useCallback((key, ...args)=>{
|
|
100
|
+
if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
|
|
101
|
+
return i18nInstance.t(key, ...args);
|
|
102
|
+
}, [
|
|
103
|
+
currentLanguage,
|
|
104
|
+
i18nInstance
|
|
105
|
+
]);
|
|
93
106
|
const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
|
|
94
107
|
languages
|
|
95
108
|
]);
|
|
@@ -123,6 +136,7 @@ const useModernI18n = ()=>{
|
|
|
123
136
|
return {
|
|
124
137
|
language: currentLanguage,
|
|
125
138
|
changeLanguage,
|
|
139
|
+
t,
|
|
126
140
|
i18nInstance,
|
|
127
141
|
supportedLanguages: languages || [],
|
|
128
142
|
localisedUrls,
|
|
@@ -8,13 +8,25 @@ import { ModernI18nProvider, useModernI18n } from "./context.mjs";
|
|
|
8
8
|
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
9
9
|
import { getI18nInstance } from "./i18n/index.mjs";
|
|
10
10
|
import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
11
|
-
import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
|
|
12
11
|
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
13
12
|
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
14
13
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
15
|
-
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
16
14
|
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
17
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
|
+
}
|
|
18
30
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
19
31
|
name: '@modern-js/plugin-i18n',
|
|
20
32
|
setup: (api)=>{
|
|
@@ -28,6 +40,7 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
28
40
|
return loadReactI18nextIntegration?.() ?? null;
|
|
29
41
|
};
|
|
30
42
|
api.onBeforeRender(async (context)=>{
|
|
43
|
+
const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
|
|
31
44
|
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
32
45
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
33
46
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
@@ -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 };
|
|
@@ -19,6 +19,7 @@ export declare const ModernI18nProvider: FC<ModernI18nProviderProps>;
|
|
|
19
19
|
export interface UseModernI18nReturn {
|
|
20
20
|
language: string;
|
|
21
21
|
changeLanguage: (newLang: string) => Promise<void>;
|
|
22
|
+
t: (key: string | string[], ...args: any[]) => string;
|
|
22
23
|
i18nInstance: I18nInstance;
|
|
23
24
|
supportedLanguages: string[];
|
|
24
25
|
localisedUrls?: LocalisedUrlsOption;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import Backend from 'i18next-fs-backend/cjs';
|
|
2
1
|
import type { ExtendedBackendOptions } from '../../../shared/type';
|
|
3
2
|
import type { I18nInstance } from '../instance';
|
|
3
|
+
type BackendConstructor = new (...args: any[]) => any;
|
|
4
|
+
export declare const resolveFsBackendConstructor: (backendModule: unknown) => BackendConstructor;
|
|
5
|
+
declare const Backend: BackendConstructor;
|
|
4
6
|
/**
|
|
5
7
|
* Wrapper for FS backend to add a no-op save method
|
|
6
8
|
* This is required for i18next-chained-backend to trigger refresh logic
|
|
@@ -11,3 +13,4 @@ export declare class FsBackendWithSave extends Backend {
|
|
|
11
13
|
}
|
|
12
14
|
export declare const HttpBackendWithSave: typeof FsBackendWithSave;
|
|
13
15
|
export declare const useI18nextBackend: (i18nInstance: I18nInstance, backend?: ExtendedBackendOptions) => void;
|
|
16
|
+
export {};
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.4.0-ultramodern.
|
|
20
|
+
"version": "3.4.0-ultramodern.20",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -97,15 +97,15 @@
|
|
|
97
97
|
"i18next-fs-backend": "^2.6.6",
|
|
98
98
|
"i18next-http-backend": "^4.0.0",
|
|
99
99
|
"i18next-http-middleware": "^3.9.7",
|
|
100
|
-
"@modern-js/
|
|
101
|
-
"@modern-js/server-
|
|
102
|
-
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.
|
|
103
|
-
"@modern-js/
|
|
104
|
-
"@modern-js/
|
|
105
|
-
"@modern-js/
|
|
100
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.20",
|
|
101
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.20",
|
|
102
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.20",
|
|
103
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.20",
|
|
104
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.20",
|
|
105
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.20"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
|
-
"@modern-js/runtime": "3.4.0-ultramodern.
|
|
108
|
+
"@modern-js/runtime": "3.4.0-ultramodern.20",
|
|
109
109
|
"i18next": ">=26.3.1",
|
|
110
110
|
"react": "^19.2.7",
|
|
111
111
|
"react-dom": "^19.2.7",
|
|
@@ -129,8 +129,8 @@
|
|
|
129
129
|
"react-i18next": "17.0.8",
|
|
130
130
|
"ts-node": "^10.9.2",
|
|
131
131
|
"typescript": "^6.0.3",
|
|
132
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.
|
|
133
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.
|
|
132
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.4.0-ultramodern.20",
|
|
133
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.20"
|
|
134
134
|
},
|
|
135
135
|
"sideEffects": false,
|
|
136
136
|
"publishConfig": {
|
package/src/runtime/context.tsx
CHANGED
|
@@ -32,7 +32,24 @@ export interface ModernI18nContextValue {
|
|
|
32
32
|
updateLanguage?: (newLang: string) => void;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const modernI18nContextKey = Symbol.for(
|
|
36
|
+
'@modern-js/plugin-i18n/runtime/ModernI18nContext',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
type ModernI18nGlobal = typeof globalThis & {
|
|
40
|
+
[key: symbol]:
|
|
41
|
+
| ReturnType<typeof createContext<ModernI18nContextValue | null>>
|
|
42
|
+
| undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getModernI18nContext = () => {
|
|
46
|
+
const globalStore = globalThis as ModernI18nGlobal;
|
|
47
|
+
globalStore[modernI18nContextKey] ??=
|
|
48
|
+
createContext<ModernI18nContextValue | null>(null);
|
|
49
|
+
return globalStore[modernI18nContextKey];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ModernI18nContext = getModernI18nContext();
|
|
36
53
|
|
|
37
54
|
export interface ModernI18nProviderProps {
|
|
38
55
|
children: ReactNode;
|
|
@@ -53,6 +70,7 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
|
|
|
53
70
|
export interface UseModernI18nReturn {
|
|
54
71
|
language: string;
|
|
55
72
|
changeLanguage: (newLang: string) => Promise<void>;
|
|
73
|
+
t: (key: string | string[], ...args: any[]) => string;
|
|
56
74
|
i18nInstance: I18nInstance;
|
|
57
75
|
supportedLanguages: string[];
|
|
58
76
|
localisedUrls?: LocalisedUrlsOption;
|
|
@@ -250,6 +268,17 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
250
268
|
],
|
|
251
269
|
);
|
|
252
270
|
|
|
271
|
+
const t = useCallback(
|
|
272
|
+
(key: string | string[], ...args: any[]) => {
|
|
273
|
+
if (typeof i18nInstance.t !== 'function') {
|
|
274
|
+
throw new Error('i18nInstance.t is required');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return i18nInstance.t(key, ...args) as string;
|
|
278
|
+
},
|
|
279
|
+
[currentLanguage, i18nInstance],
|
|
280
|
+
);
|
|
281
|
+
|
|
253
282
|
// Helper function to check if a language is supported
|
|
254
283
|
const isLanguageSupported = useCallback(
|
|
255
284
|
(lang: string) => {
|
|
@@ -310,6 +339,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
310
339
|
return {
|
|
311
340
|
language: currentLanguage,
|
|
312
341
|
changeLanguage,
|
|
342
|
+
t,
|
|
313
343
|
i18nInstance,
|
|
314
344
|
supportedLanguages: languages || [],
|
|
315
345
|
localisedUrls,
|
package/src/runtime/core.tsx
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
23
23
|
import { getI18nInstance } from './i18n';
|
|
24
24
|
import { mergeBackendOptions } from './i18n/backend';
|
|
25
|
-
import { useI18nextBackend } from './i18n/backend/middleware';
|
|
26
25
|
import {
|
|
27
26
|
detectLanguageWithPriority,
|
|
28
27
|
exportServerLngToWindow,
|
|
@@ -30,18 +29,37 @@ import {
|
|
|
30
29
|
} from './i18n/detection';
|
|
31
30
|
import { useI18nextLanguageDetector } from './i18n/detection/middleware';
|
|
32
31
|
import { getI18nextInstanceForProvider } from './i18n/instance';
|
|
33
|
-
import {
|
|
34
|
-
changeI18nLanguage,
|
|
35
|
-
ensureLanguageMatch,
|
|
36
|
-
initializeI18nInstance,
|
|
37
|
-
setupClonedInstance,
|
|
38
|
-
} from './i18n/utils';
|
|
39
32
|
import { getPathname } from './utils';
|
|
40
33
|
import './types';
|
|
41
34
|
|
|
42
35
|
export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
|
|
43
36
|
export type { Resources } from './i18n/instance';
|
|
44
37
|
|
|
38
|
+
type I18nLifecycleHelpers = {
|
|
39
|
+
useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
|
|
40
|
+
changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
|
|
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
|
+
}
|
|
62
|
+
|
|
45
63
|
export interface I18nPluginOptions {
|
|
46
64
|
entryName?: string;
|
|
47
65
|
localeDetection?: BaseLocaleDetectionOptions;
|
|
@@ -104,6 +122,13 @@ export const createI18nPlugin =
|
|
|
104
122
|
};
|
|
105
123
|
|
|
106
124
|
api.onBeforeRender(async context => {
|
|
125
|
+
const {
|
|
126
|
+
useI18nextBackend,
|
|
127
|
+
changeI18nLanguage,
|
|
128
|
+
ensureLanguageMatch,
|
|
129
|
+
initializeI18nInstance,
|
|
130
|
+
setupClonedInstance,
|
|
131
|
+
} = await loadI18nLifecycleHelpers();
|
|
107
132
|
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
108
133
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
109
134
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import FsBackendModule from 'i18next-fs-backend';
|
|
2
2
|
import type { ExtendedBackendOptions } from '../../../shared/type';
|
|
3
3
|
import type { I18nInstance } from '../instance';
|
|
4
4
|
import { useI18nextBackendCommon } from './middleware.common';
|
|
5
5
|
|
|
6
|
+
type BackendConstructor = new (...args: any[]) => any;
|
|
7
|
+
|
|
8
|
+
export const resolveFsBackendConstructor = (
|
|
9
|
+
backendModule: unknown,
|
|
10
|
+
): BackendConstructor => {
|
|
11
|
+
const nestedDefault = (backendModule as { default?: { default?: unknown } })
|
|
12
|
+
?.default?.default;
|
|
13
|
+
const nestedModuleExports = (
|
|
14
|
+
backendModule as { default?: { 'module.exports'?: unknown } }
|
|
15
|
+
)?.default?.['module.exports'];
|
|
16
|
+
const candidates = [
|
|
17
|
+
backendModule,
|
|
18
|
+
(backendModule as { default?: unknown })?.default,
|
|
19
|
+
(backendModule as { 'module.exports'?: unknown })?.['module.exports'],
|
|
20
|
+
nestedDefault,
|
|
21
|
+
nestedModuleExports,
|
|
22
|
+
];
|
|
23
|
+
const Backend = candidates.find(candidate => typeof candidate === 'function');
|
|
24
|
+
|
|
25
|
+
if (!Backend) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Failed to resolve i18next-fs-backend constructor for the i18n Node backend.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Backend as BackendConstructor;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const Backend = resolveFsBackendConstructor(FsBackendModule);
|
|
35
|
+
|
|
6
36
|
/**
|
|
7
37
|
* Wrapper for FS backend to add a no-op save method
|
|
8
38
|
* This is required for i18next-chained-backend to trigger refresh logic
|
package/tests/i18nUtils.test.ts
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
DEFAULT_I18NEXT_BACKEND_OPTIONS as NODE_DEFAULT_I18NEXT_BACKEND_OPTIONS,
|
|
5
5
|
resolveDefaultLocalesDir,
|
|
6
6
|
} from '../src/runtime/i18n/backend/defaults.node';
|
|
7
|
+
import {
|
|
8
|
+
FsBackendWithSave,
|
|
9
|
+
resolveFsBackendConstructor,
|
|
10
|
+
} from '../src/runtime/i18n/backend/middleware.node';
|
|
7
11
|
import { initializeI18nInstance } from '../src/runtime/i18n/utils';
|
|
8
12
|
|
|
9
13
|
function createBackendI18nInstance(): I18nInstance {
|
|
@@ -20,6 +24,36 @@ function createBackendI18nInstance(): I18nInstance {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
describe('i18n runtime utils', () => {
|
|
27
|
+
test('normalizes node fs backend CJS and ESM namespace shapes', () => {
|
|
28
|
+
class FakeBackend {}
|
|
29
|
+
|
|
30
|
+
expect(resolveFsBackendConstructor(FakeBackend)).toBe(FakeBackend);
|
|
31
|
+
expect(resolveFsBackendConstructor({ default: FakeBackend })).toBe(
|
|
32
|
+
FakeBackend,
|
|
33
|
+
);
|
|
34
|
+
expect(resolveFsBackendConstructor({ 'module.exports': FakeBackend })).toBe(
|
|
35
|
+
FakeBackend,
|
|
36
|
+
);
|
|
37
|
+
expect(
|
|
38
|
+
resolveFsBackendConstructor({ default: { default: FakeBackend } }),
|
|
39
|
+
).toBe(FakeBackend);
|
|
40
|
+
expect(
|
|
41
|
+
resolveFsBackendConstructor({
|
|
42
|
+
default: { 'module.exports': FakeBackend },
|
|
43
|
+
}),
|
|
44
|
+
).toBe(FakeBackend);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('node fs backend wrapper extends a resolved constructor', () => {
|
|
48
|
+
const backend = new FsBackendWithSave({}, {}, {}) as {
|
|
49
|
+
type?: string;
|
|
50
|
+
save: (language: string, namespace: string, data: unknown) => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(backend.type).toBe('backend');
|
|
54
|
+
expect(() => backend.save('en', 'translation', {})).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
23
57
|
test('node fs backend defaults follow the detected locales directory', () => {
|
|
24
58
|
// The default must match whichever conventional root exists at runtime
|
|
25
59
|
// (./locales first, then ./config/public/locales), mirroring the CLI
|
|
@@ -19,6 +19,24 @@ describe('react-i18next runtime boundary', () => {
|
|
|
19
19
|
expect(core).not.toContain("import('react-i18next')");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
test('keeps the runtime plugin factory entry synchronous for federation', () => {
|
|
23
|
+
const core = readRuntimeSource('core.tsx');
|
|
24
|
+
|
|
25
|
+
expect(core).not.toContain("from './i18n/backend/middleware'");
|
|
26
|
+
expect(core).not.toContain("from './i18n/utils'");
|
|
27
|
+
expect(core).toContain("import('./i18n/backend/middleware')");
|
|
28
|
+
expect(core).toContain("import('./i18n/utils')");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('keeps the Modern i18n context stable across federated runtime copies', () => {
|
|
32
|
+
const context = readRuntimeSource('context.tsx');
|
|
33
|
+
|
|
34
|
+
expect(context).toContain(
|
|
35
|
+
"Symbol.for(\n '@modern-js/plugin-i18n/runtime/ModernI18nContext'",
|
|
36
|
+
);
|
|
37
|
+
expect(context).toContain('globalStore[modernI18nContextKey] ??=');
|
|
38
|
+
});
|
|
39
|
+
|
|
22
40
|
test('keeps the default runtime entry wired to react-i18next integration', () => {
|
|
23
41
|
const defaultEntry = readRuntimeSource('index.tsx');
|
|
24
42
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from '@modern-js/runtime/context';
|
|
6
6
|
import type React from 'react';
|
|
7
7
|
import type { ComponentType, PropsWithChildren } from 'react';
|
|
8
|
-
import { act } from 'react';
|
|
8
|
+
import { act, useState } from 'react';
|
|
9
9
|
import { createRoot, type Root } from 'react-dom/client';
|
|
10
10
|
import { i18nPlugin } from '../src/runtime';
|
|
11
11
|
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
@@ -453,4 +453,61 @@ describe('i18n router adapter', () => {
|
|
|
453
453
|
replace: true,
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
test('exposes a language-scoped t function for rendered copy', async () => {
|
|
458
|
+
const i18nInstance = createI18nInstance('en');
|
|
459
|
+
i18nInstance.t = (key: string) => `${i18nInstance.language}:${key}`;
|
|
460
|
+
const renderTranslations: Array<(key: string) => string> = [];
|
|
461
|
+
let setProviderLanguage: ((language: string) => void) | undefined;
|
|
462
|
+
|
|
463
|
+
const StatefulI18nProvider = ({ children }: PropsWithChildren) => {
|
|
464
|
+
const [language, setLanguage] = useState('en');
|
|
465
|
+
setProviderLanguage = setLanguage;
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<ModernI18nProvider
|
|
469
|
+
value={{
|
|
470
|
+
language,
|
|
471
|
+
i18nInstance,
|
|
472
|
+
languages: ['en', 'cs'],
|
|
473
|
+
localePathRedirect: true,
|
|
474
|
+
localisedUrls,
|
|
475
|
+
updateLanguage: setLanguage,
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
{children}
|
|
479
|
+
</ModernI18nProvider>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const Harness = () => {
|
|
484
|
+
const { t } = useModernI18n();
|
|
485
|
+
renderTranslations.push(t);
|
|
486
|
+
return <span data-testid="translation">{t('key')}</span>;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
rendered = await renderWithRuntime(
|
|
490
|
+
<StatefulI18nProvider>
|
|
491
|
+
<Harness />
|
|
492
|
+
</StatefulI18nProvider>,
|
|
493
|
+
createReactRouterRuntimeContext({ navigate: rstest.fn() }),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(
|
|
497
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
498
|
+
?.textContent,
|
|
499
|
+
).toBe('en:key');
|
|
500
|
+
const initialT = renderTranslations.at(-1);
|
|
501
|
+
|
|
502
|
+
await act(async () => {
|
|
503
|
+
i18nInstance.language = 'cs';
|
|
504
|
+
setProviderLanguage?.('cs');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(
|
|
508
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
509
|
+
?.textContent,
|
|
510
|
+
).toBe('cs:key');
|
|
511
|
+
expect(renderTranslations.at(-1)).not.toBe(initialT);
|
|
512
|
+
});
|
|
456
513
|
});
|