@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.82 → 3.2.0-ultramodern.84

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.
@@ -28,8 +28,8 @@ __webpack_require__.d(__webpack_exports__, {
28
28
  convertBackendOptions: ()=>convertBackendOptions
29
29
  });
30
30
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
31
- loadPath: './locales/{{lng}}/{{ns}}.json',
32
- addPath: './locales/{{lng}}/{{ns}}.json'
31
+ loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
32
+ addPath: './config/public/locales/{{lng}}/{{ns}}.json'
33
33
  };
34
34
  function convertPath(path) {
35
35
  if (!path) return path;
@@ -26,12 +26,9 @@ __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
27
  getReactI18nextIntegration: ()=>getReactI18nextIntegration
28
28
  });
29
- function getOptionalReactI18nextPackageName() {
30
- return "react-i18next";
31
- }
32
29
  async function tryImportReactI18next() {
33
30
  try {
34
- return await import(getOptionalReactI18nextPackageName());
31
+ return await import("react-i18next");
35
32
  } catch (error) {
36
33
  return null;
37
34
  }
@@ -137,6 +137,7 @@ const i18nPlugin = (options)=>({
137
137
  localisedUrls,
138
138
  forceUpdate
139
139
  ]);
140
+ const children = props.children;
140
141
  const appContent = /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(jsx_runtime_namespaceObject.Fragment, {
141
142
  children: [
142
143
  Boolean(htmlLangAttr) && /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(head_namespaceObject.Helmet, {
@@ -146,9 +147,10 @@ const i18nPlugin = (options)=>({
146
147
  }),
147
148
  /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_context_js_namespaceObject.ModernI18nProvider, {
148
149
  value: contextValue,
149
- children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(App, {
150
- ...props
151
- })
150
+ children: App ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(App, {
151
+ ...props,
152
+ children: children
153
+ }) : children
152
154
  })
153
155
  ]
154
156
  });
@@ -1,6 +1,6 @@
1
1
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
2
- loadPath: './locales/{{lng}}/{{ns}}.json',
3
- addPath: './locales/{{lng}}/{{ns}}.json'
2
+ loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
3
+ addPath: './config/public/locales/{{lng}}/{{ns}}.json'
4
4
  };
5
5
  function convertPath(path) {
6
6
  if (!path) return path;
@@ -1,9 +1,6 @@
1
- function getOptionalReactI18nextPackageName() {
2
- return "react-i18next";
3
- }
4
1
  async function tryImportReactI18next() {
5
2
  try {
6
- return await import(getOptionalReactI18nextPackageName());
3
+ return await import("react-i18next");
7
4
  } catch (error) {
8
5
  return null;
9
6
  }
@@ -105,6 +105,7 @@ const i18nPlugin = (options)=>({
105
105
  localisedUrls,
106
106
  forceUpdate
107
107
  ]);
108
+ const children = props.children;
108
109
  const appContent = /*#__PURE__*/ jsxs(Fragment, {
109
110
  children: [
110
111
  Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
@@ -114,9 +115,10 @@ const i18nPlugin = (options)=>({
114
115
  }),
115
116
  /*#__PURE__*/ jsx(ModernI18nProvider, {
116
117
  value: contextValue,
117
- children: /*#__PURE__*/ jsx(App, {
118
- ...props
119
- })
118
+ children: App ? /*#__PURE__*/ jsx(App, {
119
+ ...props,
120
+ children: children
121
+ }) : children
120
122
  })
121
123
  ]
122
124
  });
@@ -1,7 +1,7 @@
1
1
  import "node:module";
2
2
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
3
- loadPath: './locales/{{lng}}/{{ns}}.json',
4
- addPath: './locales/{{lng}}/{{ns}}.json'
3
+ loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
4
+ addPath: './config/public/locales/{{lng}}/{{ns}}.json'
5
5
  };
6
6
  function convertPath(path) {
7
7
  if (!path) return path;
@@ -1,10 +1,7 @@
1
1
  import "node:module";
2
- function getOptionalReactI18nextPackageName() {
3
- return "react-i18next";
4
- }
5
2
  async function tryImportReactI18next() {
6
3
  try {
7
- return await import(getOptionalReactI18nextPackageName());
4
+ return await import("react-i18next");
8
5
  } catch (error) {
9
6
  return null;
10
7
  }
@@ -106,6 +106,7 @@ const i18nPlugin = (options)=>({
106
106
  localisedUrls,
107
107
  forceUpdate
108
108
  ]);
109
+ const children = props.children;
109
110
  const appContent = /*#__PURE__*/ jsxs(Fragment, {
110
111
  children: [
111
112
  Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
@@ -115,9 +116,10 @@ const i18nPlugin = (options)=>({
115
116
  }),
116
117
  /*#__PURE__*/ jsx(ModernI18nProvider, {
117
118
  value: contextValue,
118
- children: /*#__PURE__*/ jsx(App, {
119
- ...props
120
- })
119
+ children: App ? /*#__PURE__*/ jsx(App, {
120
+ ...props,
121
+ children: children
122
+ }) : children
121
123
  })
122
124
  ]
123
125
  });
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.2.0-ultramodern.82",
20
+ "version": "3.2.0-ultramodern.84",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -81,32 +81,29 @@
81
81
  }
82
82
  },
83
83
  "dependencies": {
84
- "@swc/helpers": "^0.5.21",
84
+ "@swc/helpers": "^0.5.23",
85
85
  "i18next-browser-languagedetector": "^8.2.1",
86
86
  "i18next-chained-backend": "^5.0.4",
87
87
  "i18next-fs-backend": "^2.6.6",
88
88
  "i18next-http-backend": "^4.0.0",
89
89
  "i18next-http-middleware": "^3.9.7",
90
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.82",
91
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.2.0-ultramodern.82",
92
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.82",
93
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.2.0-ultramodern.82",
94
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.2.0-ultramodern.82",
95
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.2.0-ultramodern.82"
90
+ "react-i18next": "17.0.8",
91
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.84",
92
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.2.0-ultramodern.84",
93
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.84",
94
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.2.0-ultramodern.84",
95
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.2.0-ultramodern.84",
96
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.2.0-ultramodern.84"
96
97
  },
97
98
  "peerDependencies": {
98
- "@modern-js/runtime": "3.2.0-ultramodern.82",
99
+ "@modern-js/runtime": "3.2.0-ultramodern.84",
99
100
  "i18next": ">=25.7.4",
100
101
  "react": "^19.2.6",
101
- "react-dom": "^19.2.6",
102
- "react-i18next": ">=15.7.4"
102
+ "react-dom": "^19.2.6"
103
103
  },
104
104
  "peerDependenciesMeta": {
105
105
  "i18next": {
106
106
  "optional": true
107
- },
108
- "react-i18next": {
109
- "optional": true
110
107
  }
111
108
  },
112
109
  "devDependencies": {
@@ -118,10 +115,9 @@
118
115
  "jest": "^30.4.2",
119
116
  "react": "^19.2.6",
120
117
  "react-dom": "^19.2.6",
121
- "react-i18next": "17.0.8",
122
118
  "ts-jest": "^29.4.11",
123
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.82",
124
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.82"
119
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.84",
120
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.84"
125
121
  },
126
122
  "sideEffects": false,
127
123
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  export const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
2
- loadPath: './locales/{{lng}}/{{ns}}.json',
3
- addPath: './locales/{{lng}}/{{ns}}.json',
2
+ loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
3
+ addPath: './config/public/locales/{{lng}}/{{ns}}.json',
4
4
  };
5
5
 
6
6
  function convertPath(path: string | undefined): string | undefined {
@@ -7,15 +7,9 @@ interface ReactI18nextIntegration {
7
7
  initReactI18next: any | null;
8
8
  }
9
9
 
10
- function getOptionalReactI18nextPackageName(): string {
11
- return ['react', 'i18next'].join('-');
12
- }
13
-
14
10
  async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
15
11
  try {
16
- return (await import(
17
- getOptionalReactI18nextPackageName()
18
- )) as ReactI18nextModule;
12
+ return (await import('react-i18next')) as ReactI18nextModule;
19
13
  } catch (error) {
20
14
  return null;
21
15
  }
@@ -263,11 +263,12 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
263
263
  ],
264
264
  );
265
265
 
266
+ const children = (props as React.PropsWithChildren).children;
266
267
  const appContent = (
267
268
  <>
268
269
  {Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
269
270
  <ModernI18nProvider value={contextValue}>
270
- <App {...props} />
271
+ {App ? <App {...props}>{children}</App> : children}
271
272
  </ModernI18nProvider>
272
273
  </>
273
274
  );
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from '@rstest/core';
2
2
  import type { I18nInstance } from '../src/runtime/i18n';
3
+ import { DEFAULT_I18NEXT_BACKEND_OPTIONS as NODE_DEFAULT_I18NEXT_BACKEND_OPTIONS } from '../src/runtime/i18n/backend/defaults.node';
3
4
  import { initializeI18nInstance } from '../src/runtime/i18n/utils';
4
5
 
5
6
  function createBackendI18nInstance(): I18nInstance {
@@ -16,6 +17,12 @@ function createBackendI18nInstance(): I18nInstance {
16
17
  }
17
18
 
18
19
  describe('i18n runtime utils', () => {
20
+ test('uses the generated public locale directory for node fs backend defaults', () => {
21
+ expect(NODE_DEFAULT_I18NEXT_BACKEND_OPTIONS.loadPath).toBe(
22
+ './config/public/locales/{{lng}}/{{ns}}.json',
23
+ );
24
+ });
25
+
19
26
  test('does not poll for backend resources after init', async () => {
20
27
  const i18nInstance = createBackendI18nInstance();
21
28
  const init = rstest.fn(async () => {
@@ -1,10 +1,16 @@
1
- import { InternalRuntimeContext } from '@modern-js/runtime/context';
1
+ import {
2
+ InternalRuntimeContext,
3
+ RuntimeContext,
4
+ } from '@modern-js/runtime/context';
2
5
  import type React from 'react';
6
+ import type { ComponentType, PropsWithChildren } from 'react';
3
7
  import { act } from 'react';
4
8
  import { createRoot, type Root } from 'react-dom/client';
9
+ import { i18nPlugin } from '../src/runtime';
5
10
  import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
6
11
  import { I18nLink } from '../src/runtime/I18nLink';
7
12
  import type { I18nInstance } from '../src/runtime/i18n';
13
+ import { getReactI18nextIntegration } from '../src/runtime/i18n/react-i18next';
8
14
 
9
15
  (
10
16
  globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
@@ -72,6 +78,29 @@ function createReactRouterRuntimeContext(router: unknown) {
72
78
  return createRuntimeContext(router, 'react-router');
73
79
  }
74
80
 
81
+ function collectI18nWrapRoot() {
82
+ let wrapRoot: ((App: ComponentType<any>) => ComponentType<any>) | undefined;
83
+
84
+ i18nPlugin({
85
+ reactI18next: false,
86
+ localeDetection: {
87
+ fallbackLanguage: 'en',
88
+ },
89
+ }).setup?.({
90
+ getRuntimeConfig: () => ({}),
91
+ onBeforeRender: () => undefined,
92
+ wrapRoot: (callback: (App: ComponentType<any>) => ComponentType<any>) => {
93
+ wrapRoot = callback;
94
+ },
95
+ } as any);
96
+
97
+ if (!wrapRoot) {
98
+ throw new Error('Expected i18n runtime plugin to register wrapRoot');
99
+ }
100
+
101
+ return wrapRoot;
102
+ }
103
+
75
104
  function createTanstackRouter(pathname = '/en/terms-of-service', lang = 'en') {
76
105
  const url = new URL(pathname, 'https://modernjs.test');
77
106
 
@@ -94,6 +123,31 @@ function createTanstackRouter(pathname = '/en/terms-of-service', lang = 'en') {
94
123
  };
95
124
  }
96
125
 
126
+ async function renderI18nRoot(node: React.ReactNode) {
127
+ const container = document.createElement('div');
128
+ document.body.appendChild(container);
129
+ const root = createRoot(container);
130
+
131
+ await act(async () => {
132
+ root.render(
133
+ <RuntimeContext.Provider
134
+ value={{
135
+ isBrowser: true,
136
+ requestContext,
137
+ context: requestContext,
138
+ }}
139
+ >
140
+ {node}
141
+ </RuntimeContext.Provider>,
142
+ );
143
+ });
144
+
145
+ return {
146
+ container,
147
+ root,
148
+ };
149
+ }
150
+
97
151
  async function renderWithRuntime(
98
152
  node: React.ReactNode,
99
153
  runtimeContext: ReturnType<typeof createTanstackRuntimeContext>,
@@ -126,6 +180,56 @@ function cleanup(rendered?: { container: HTMLElement; root: Root }) {
126
180
  rendered.container.remove();
127
181
  }
128
182
 
183
+ describe('i18n runtime wrapRoot', () => {
184
+ let rendered: { container: HTMLElement; root: Root } | undefined;
185
+
186
+ afterEach(() => {
187
+ cleanup(rendered);
188
+ rendered = undefined;
189
+ });
190
+
191
+ test('renders children when no root App exists yet', async () => {
192
+ const wrapRoot = collectI18nWrapRoot();
193
+ const I18nRoot = wrapRoot(undefined as unknown as ComponentType<any>);
194
+
195
+ rendered = await renderI18nRoot(
196
+ <I18nRoot>
197
+ <main>router content</main>
198
+ </I18nRoot>,
199
+ );
200
+
201
+ expect(rendered.container.textContent).toContain('router content');
202
+ });
203
+
204
+ test('preserves App props and children', async () => {
205
+ const wrapRoot = collectI18nWrapRoot();
206
+ const App = ({ children, label }: PropsWithChildren<{ label: string }>) => (
207
+ <main data-label={label}>{children}</main>
208
+ );
209
+ const I18nRoot = wrapRoot(App);
210
+
211
+ rendered = await renderI18nRoot(
212
+ <I18nRoot label="root">
213
+ <span>router content</span>
214
+ </I18nRoot>,
215
+ );
216
+
217
+ expect(
218
+ rendered.container.querySelector('main')?.getAttribute('data-label'),
219
+ ).toBe('root');
220
+ expect(rendered.container.textContent).toContain('router content');
221
+ });
222
+ });
223
+
224
+ describe('i18n react-i18next integration', () => {
225
+ test('loads the bundled react-i18next integration', async () => {
226
+ const integration = await getReactI18nextIntegration();
227
+
228
+ expect(integration.I18nextProvider).toEqual(expect.any(Function));
229
+ expect(integration.initReactI18next).toBeDefined();
230
+ });
231
+ });
232
+
129
233
  describe('i18n router adapter', () => {
130
234
  let rendered: { container: HTMLElement; root: Root } | undefined;
131
235