@bleedingdev/modern-js-plugin-i18n 3.4.0-ultramodern.2 → 3.4.0-ultramodern.4

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.
@@ -122,6 +122,13 @@ const useModernI18n = ()=>{
122
122
  navigate,
123
123
  location
124
124
  ]);
125
+ const t = (0, external_react_namespaceObject.useCallback)((key, ...args)=>{
126
+ if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
127
+ return i18nInstance.t(key, ...args);
128
+ }, [
129
+ currentLanguage,
130
+ i18nInstance
131
+ ]);
125
132
  const isLanguageSupported = (0, external_react_namespaceObject.useCallback)((lang)=>languages?.includes(lang) || false, [
126
133
  languages
127
134
  ]);
@@ -155,6 +162,7 @@ const useModernI18n = ()=>{
155
162
  return {
156
163
  language: currentLanguage,
157
164
  changeLanguage,
165
+ t,
158
166
  i18nInstance,
159
167
  supportedLanguages: languages || [],
160
168
  localisedUrls,
@@ -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', {
@@ -89,6 +89,13 @@ const useModernI18n = ()=>{
89
89
  navigate,
90
90
  location
91
91
  ]);
92
+ const t = useCallback((key, ...args)=>{
93
+ if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
94
+ return i18nInstance.t(key, ...args);
95
+ }, [
96
+ currentLanguage,
97
+ i18nInstance
98
+ ]);
92
99
  const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
93
100
  languages
94
101
  ]);
@@ -122,6 +129,7 @@ const useModernI18n = ()=>{
122
129
  return {
123
130
  language: currentLanguage,
124
131
  changeLanguage,
132
+ t,
125
133
  i18nInstance,
126
134
  supportedLanguages: languages || [],
127
135
  localisedUrls,
@@ -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 };
@@ -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,
@@ -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.4",
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/plugin": "npm:@bleedingdev/modern-js-plugin@3.4.0-ultramodern.4",
101
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.4.0-ultramodern.4",
102
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.4.0-ultramodern.4",
103
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.4.0-ultramodern.4",
104
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.4.0-ultramodern.4",
105
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.4.0-ultramodern.4"
106
106
  },
107
107
  "peerDependencies": {
108
- "@modern-js/runtime": "3.4.0-ultramodern.2",
108
+ "@modern-js/runtime": "3.4.0-ultramodern.4",
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.4",
133
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.4.0-ultramodern.4"
134
134
  },
135
135
  "sideEffects": false,
136
136
  "publishConfig": {
@@ -53,6 +53,7 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
53
53
  export interface UseModernI18nReturn {
54
54
  language: string;
55
55
  changeLanguage: (newLang: string) => Promise<void>;
56
+ t: (key: string | string[], ...args: any[]) => string;
56
57
  i18nInstance: I18nInstance;
57
58
  supportedLanguages: string[];
58
59
  localisedUrls?: LocalisedUrlsOption;
@@ -250,6 +251,17 @@ export const useModernI18n = (): UseModernI18nReturn => {
250
251
  ],
251
252
  );
252
253
 
254
+ const t = useCallback(
255
+ (key: string | string[], ...args: any[]) => {
256
+ if (typeof i18nInstance.t !== 'function') {
257
+ throw new Error('i18nInstance.t is required');
258
+ }
259
+
260
+ return i18nInstance.t(key, ...args) as string;
261
+ },
262
+ [currentLanguage, i18nInstance],
263
+ );
264
+
253
265
  // Helper function to check if a language is supported
254
266
  const isLanguageSupported = useCallback(
255
267
  (lang: string) => {
@@ -310,6 +322,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
310
322
  return {
311
323
  language: currentLanguage,
312
324
  changeLanguage,
325
+ t,
313
326
  i18nInstance,
314
327
  supportedLanguages: languages || [],
315
328
  localisedUrls,
@@ -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
@@ -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
  });