@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.
@@ -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 ModernI18nContext = /*#__PURE__*/ (0, external_react_namespaceObject.createContext)(null);
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,
@@ -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 detection_middleware_js_namespaceObject = require("./i18n/detection/middleware.js");
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, detection_middleware_js_namespaceObject.useI18nextLanguageDetector)(i18nInstance);
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)) (0, middleware_js_namespaceObject.useI18nextBackend)(i18nInstance, mergedBackend);
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 (0, utils_js_namespaceObject.initializeI18nInstance)(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
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 (0, utils_js_namespaceObject.setupClonedInstance)(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
114
+ await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
102
115
  }
103
- if (localePathRedirect) await (0, utils_js_namespaceObject.ensureLanguageMatch)(i18nInstance, finalLanguage);
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 (0, utils_js_namespaceObject.changeI18nLanguage)(i18nInstance, newLang, {
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 cjs_namespaceObject = require("i18next-fs-backend/cjs");
45
- var cjs_default = /*#__PURE__*/ __webpack_require__.n(cjs_namespaceObject);
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
- class FsBackendWithSave extends cjs_default() {
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, cjs_default(), backend);
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 ModernI18nContext = /*#__PURE__*/ createContext(null);
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 cjs from "i18next-fs-backend/cjs";
1
+ import i18next_fs_backend from "i18next-fs-backend";
2
2
  import { useI18nextBackendCommon } from "./middleware.common.mjs";
3
- class FsBackendWithSave extends cjs {
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, cjs, backend);
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 ModernI18nContext = /*#__PURE__*/ createContext(null);
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 cjs from "i18next-fs-backend/cjs";
2
+ import i18next_fs_backend from "i18next-fs-backend";
3
3
  import { useI18nextBackendCommon } from "./middleware.common.mjs";
4
- class FsBackendWithSave extends cjs {
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, cjs, backend);
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.2",
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/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.2",
101
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.2",
102
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.2",
103
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.2",
104
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.2",
105
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.2"
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.2",
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.2",
133
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.2"
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": {
@@ -32,7 +32,24 @@ export interface ModernI18nContextValue {
32
32
  updateLanguage?: (newLang: string) => void;
33
33
  }
34
34
 
35
- const ModernI18nContext = createContext<ModernI18nContextValue | null>(null);
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,
@@ -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 Backend from 'i18next-fs-backend/cjs';
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
@@ -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
  });