@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.83 → 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.
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +2 -2
- package/dist/cjs/runtime/i18n/react-i18next.js +1 -4
- package/dist/cjs/runtime/index.js +5 -3
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm/runtime/i18n/react-i18next.mjs +1 -4
- package/dist/esm/runtime/index.mjs +5 -3
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +1 -4
- package/dist/esm-node/runtime/index.mjs +5 -3
- package/package.json +13 -17
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/react-i18next.ts +1 -7
- package/src/runtime/index.tsx +2 -1
- package/tests/i18nUtils.test.ts +7 -0
- package/tests/routerAdapter.test.tsx +105 -1
|
@@ -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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
91
|
-
"@modern-js/
|
|
92
|
-
"@modern-js/
|
|
93
|
-
"@modern-js/
|
|
94
|
-
"@modern-js/
|
|
95
|
-
"@modern-js/
|
|
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.
|
|
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.
|
|
124
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.
|
|
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
|
}
|
package/src/runtime/index.tsx
CHANGED
|
@@ -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
|
);
|
package/tests/i18nUtils.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|