@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.8 → 3.9.3-ultramodern.1
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/cli/index.js +11 -0
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/context.js +6 -14
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +12 -2
- package/dist/cjs/runtime/i18n/detection/middleware.js +1 -1
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +10 -1
- package/dist/cjs/runtime/i18n/detection/priority.js +1 -1
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/routerAdapter.js +3 -5
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +6 -2
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +3 -2
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +13 -9
- package/dist/cjs/shared/detection.js +118 -2
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +31 -1
- package/dist/esm/cli/index.mjs +11 -0
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/context.mjs +2 -10
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +7 -3
- package/dist/esm/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm/runtime/i18n/detection/middleware.node.mjs +10 -1
- package/dist/esm/runtime/i18n/detection/priority.mjs +1 -1
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/routerAdapter.mjs +2 -4
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +6 -2
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +3 -2
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +6 -2
- package/dist/esm/shared/detection.mjs +85 -1
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +11 -0
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/context.mjs +2 -10
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +7 -3
- package/dist/esm-node/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm-node/runtime/i18n/detection/middleware.node.mjs +10 -1
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +1 -1
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +2 -4
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +6 -2
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +3 -2
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +6 -2
- package/dist/esm-node/shared/detection.mjs +85 -1
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +7 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -5
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +1 -1
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +1 -1
- package/dist/types/shared/detection.d.ts +11 -1
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +10 -0
- package/dist/types/shared/urlStrategy.d.ts +11 -0
- package/package.json +43 -24
- package/src/cli/index.ts +43 -1
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/context.tsx +6 -31
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +30 -3
- package/src/runtime/i18n/detection/middleware.node.ts +25 -7
- package/src/runtime/i18n/detection/middleware.ts +2 -0
- package/src/runtime/i18n/detection/priority.ts +2 -0
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/routerAdapter.tsx +2 -11
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +13 -4
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +7 -12
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +19 -4
- package/src/shared/detection.ts +34 -10
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +10 -0
- package/src/shared/urlStrategy.ts +27 -0
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -46
- package/tests/backendDefaults.test.ts +0 -30
- package/tests/federatedI18nBoundaryResolver.test.tsx +0 -65
- package/tests/i18nUtils.test.ts +0 -60
- package/tests/link.test.tsx +0 -139
- package/tests/localisedUrls.test.ts +0 -176
- package/tests/reactI18nextRuntimeBoundary.test.ts +0 -117
- package/tests/redirectPolicy.test.ts +0 -54
- package/tests/routerAdapter.test.tsx +0 -221
- package/tsconfig.json +0 -15
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@rstest/core';
|
|
2
|
-
import { i18nPlugin as i18nCliPlugin } from '../src/cli';
|
|
3
|
-
import {
|
|
4
|
-
collectApiPrefixes,
|
|
5
|
-
i18nServerPlugin,
|
|
6
|
-
matchesApiPrefix,
|
|
7
|
-
} from '../src/server';
|
|
8
|
-
|
|
9
|
-
const createRequestContext = (pathname: string) =>
|
|
10
|
-
({
|
|
11
|
-
req: {
|
|
12
|
-
url: `http://localhost${pathname}`,
|
|
13
|
-
},
|
|
14
|
-
}) as any;
|
|
15
|
-
|
|
16
|
-
describe('cli modifyFileSystemRoutes', () => {
|
|
17
|
-
test('uses the no-react runtime entry when reactI18next is disabled', () => {
|
|
18
|
-
let runtimePlugin:
|
|
19
|
-
| {
|
|
20
|
-
path: string;
|
|
21
|
-
config: Record<string, unknown>;
|
|
22
|
-
}
|
|
23
|
-
| undefined;
|
|
24
|
-
|
|
25
|
-
i18nCliPlugin({ reactI18next: false }).setup({
|
|
26
|
-
_internalRuntimePlugins: (fn: any) => {
|
|
27
|
-
const plugins: Array<{
|
|
28
|
-
path: string;
|
|
29
|
-
config: Record<string, unknown>;
|
|
30
|
-
}> = [];
|
|
31
|
-
fn({
|
|
32
|
-
entrypoint: { entryName: 'main' },
|
|
33
|
-
plugins,
|
|
34
|
-
});
|
|
35
|
-
runtimePlugin = plugins[0];
|
|
36
|
-
},
|
|
37
|
-
modifyFileSystemRoutes: () => {},
|
|
38
|
-
_internalServerPlugins: () => {},
|
|
39
|
-
getAppContext: () => ({
|
|
40
|
-
appDirectory: process.cwd(),
|
|
41
|
-
metaName: 'modern-js',
|
|
42
|
-
}),
|
|
43
|
-
getNormalizedConfig: () => ({}),
|
|
44
|
-
} as any);
|
|
45
|
-
|
|
46
|
-
expect(runtimePlugin?.path).toBe(
|
|
47
|
-
'@modern-js/plugin-i18n/runtime/no-react-i18next',
|
|
48
|
-
);
|
|
49
|
-
expect(runtimePlugin?.config.reactI18next).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('upstream-style configs without a map keep routes untouched', () => {
|
|
53
|
-
const modifyRoutes = rstest.fn();
|
|
54
|
-
i18nCliPlugin({
|
|
55
|
-
localeDetection: { localePathRedirect: true, languages: ['en', 'cs'] },
|
|
56
|
-
}).setup({
|
|
57
|
-
_internalRuntimePlugins: () => {},
|
|
58
|
-
_internalServerPlugins: () => {},
|
|
59
|
-
modifyFileSystemRoutes: modifyRoutes,
|
|
60
|
-
} as any);
|
|
61
|
-
expect(modifyRoutes).not.toHaveBeenCalled();
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('i18n server API prefix skips', () => {
|
|
66
|
-
test('collects API route prefixes and normalized BFF config prefixes', () => {
|
|
67
|
-
expect(
|
|
68
|
-
collectApiPrefixes(
|
|
69
|
-
[
|
|
70
|
-
{ entryName: 'main', isApi: false, urlPath: '/' },
|
|
71
|
-
{ isApi: true, urlPath: '/bff-api' },
|
|
72
|
-
{ isApi: true, urlPath: '/rpc/*' },
|
|
73
|
-
{ isApi: true, urlPath: '/' },
|
|
74
|
-
{ isApi: true },
|
|
75
|
-
],
|
|
76
|
-
['bff-api/', '/internal-api'],
|
|
77
|
-
),
|
|
78
|
-
).toEqual(['/bff-api', '/rpc', '/internal-api']);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('matches API prefixes by exact path or slash-delimited segment', () => {
|
|
82
|
-
const prefixes = ['/bff-api'];
|
|
83
|
-
|
|
84
|
-
expect(matchesApiPrefix('/bff-api', prefixes)).toBe(true);
|
|
85
|
-
expect(matchesApiPrefix('/bff-api/ping', prefixes)).toBe(true);
|
|
86
|
-
expect(matchesApiPrefix('/bff-api-v2', prefixes)).toBe(false);
|
|
87
|
-
expect(matchesApiPrefix('/bff-api-v2/ping', prefixes)).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('skips language detector and redirect middleware for API routes', async () => {
|
|
91
|
-
const middlewares: any[] = [];
|
|
92
|
-
const routes = [
|
|
93
|
-
{ entryName: 'main', entryPath: '', urlPath: '/' },
|
|
94
|
-
{ entryPath: '', isApi: true, urlPath: '/bff-api' },
|
|
95
|
-
];
|
|
96
|
-
let prepare: (() => void) | undefined;
|
|
97
|
-
|
|
98
|
-
i18nServerPlugin({
|
|
99
|
-
localeDetection: {
|
|
100
|
-
fallbackLanguage: 'en',
|
|
101
|
-
languages: ['en', 'cs'],
|
|
102
|
-
localePathRedirect: true,
|
|
103
|
-
},
|
|
104
|
-
staticRoutePrefixes: [],
|
|
105
|
-
}).setup({
|
|
106
|
-
getServerConfig: () => ({}),
|
|
107
|
-
getServerContext: () => ({ middlewares, routes }),
|
|
108
|
-
onPrepare: fn => {
|
|
109
|
-
prepare = fn;
|
|
110
|
-
},
|
|
111
|
-
} as any);
|
|
112
|
-
|
|
113
|
-
prepare?.();
|
|
114
|
-
|
|
115
|
-
const detectorMiddleware = middlewares.find(
|
|
116
|
-
middleware => middleware.name === 'i18n-language-detector',
|
|
117
|
-
);
|
|
118
|
-
const redirectMiddleware = middlewares.find(
|
|
119
|
-
middleware => middleware.name === 'i18n-server-middleware',
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
expect(detectorMiddleware).toBeDefined();
|
|
123
|
-
expect(redirectMiddleware).toBeDefined();
|
|
124
|
-
|
|
125
|
-
for (const middleware of [detectorMiddleware, redirectMiddleware]) {
|
|
126
|
-
let nextCalls = 0;
|
|
127
|
-
const response = await middleware.handler(
|
|
128
|
-
createRequestContext('/bff-api/ping'),
|
|
129
|
-
async () => {
|
|
130
|
-
nextCalls++;
|
|
131
|
-
},
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
expect(response).toBeUndefined();
|
|
135
|
-
expect(nextCalls).toBe(1);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
|
|
140
|
-
const middlewares: any[] = [];
|
|
141
|
-
const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
|
|
142
|
-
let prepare: (() => void) | undefined;
|
|
143
|
-
|
|
144
|
-
i18nServerPlugin({
|
|
145
|
-
localeDetection: {
|
|
146
|
-
fallbackLanguage: 'en',
|
|
147
|
-
languages: ['en', 'cs'],
|
|
148
|
-
localePathRedirect: true,
|
|
149
|
-
},
|
|
150
|
-
staticRoutePrefixes: [],
|
|
151
|
-
}).setup({
|
|
152
|
-
getServerConfig: () => ({ bff: {} }),
|
|
153
|
-
getServerContext: () => ({ middlewares, routes }),
|
|
154
|
-
onPrepare: fn => {
|
|
155
|
-
prepare = fn;
|
|
156
|
-
},
|
|
157
|
-
} as any);
|
|
158
|
-
|
|
159
|
-
prepare?.();
|
|
160
|
-
|
|
161
|
-
const redirectMiddleware = middlewares.find(
|
|
162
|
-
middleware => middleware.name === 'i18n-server-middleware',
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
let nextCalls = 0;
|
|
166
|
-
const response = await redirectMiddleware.handler(
|
|
167
|
-
createRequestContext('/api/ping'),
|
|
168
|
-
async () => {
|
|
169
|
-
nextCalls++;
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
expect(response).toBeUndefined();
|
|
174
|
-
expect(nextCalls).toBe(1);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { describe, expect, test } from '@rstest/core';
|
|
6
|
-
import { build } from 'esbuild';
|
|
7
|
-
import i18next from 'i18next';
|
|
8
|
-
import { createElement } from 'react';
|
|
9
|
-
import { renderToStaticMarkup } from 'react-dom/server';
|
|
10
|
-
|
|
11
|
-
const runtimeRoot = resolve(__dirname, '../src/runtime');
|
|
12
|
-
|
|
13
|
-
describe('react-i18next runtime boundary', () => {
|
|
14
|
-
test('bundles the disabled runtime entry when react-i18next is unavailable', async () => {
|
|
15
|
-
await expect(
|
|
16
|
-
build({
|
|
17
|
-
bundle: true,
|
|
18
|
-
entryPoints: [resolve(runtimeRoot, 'no-react-i18next.tsx')],
|
|
19
|
-
format: 'esm',
|
|
20
|
-
packages: 'external',
|
|
21
|
-
platform: 'node',
|
|
22
|
-
plugins: [
|
|
23
|
-
{
|
|
24
|
-
name: 'reject-react-i18next',
|
|
25
|
-
setup(buildApi) {
|
|
26
|
-
buildApi.onResolve({ filter: /^react-i18next$/ }, () => {
|
|
27
|
-
throw new Error(
|
|
28
|
-
'disabled runtime entry reached optional react-i18next',
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
write: false,
|
|
35
|
-
}),
|
|
36
|
-
).resolves.toBeDefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('gets router capabilities from the selected runtime provider', async () => {
|
|
40
|
-
await expect(
|
|
41
|
-
build({
|
|
42
|
-
bundle: true,
|
|
43
|
-
entryPoints: [resolve(runtimeRoot, 'routerAdapter.tsx')],
|
|
44
|
-
format: 'esm',
|
|
45
|
-
packages: 'external',
|
|
46
|
-
platform: 'neutral',
|
|
47
|
-
plugins: [
|
|
48
|
-
{
|
|
49
|
-
name: 'reject-direct-router-provider',
|
|
50
|
-
setup(buildApi) {
|
|
51
|
-
buildApi.onResolve(
|
|
52
|
-
{ filter: /^@modern-js\/runtime\/router$/ },
|
|
53
|
-
() => {
|
|
54
|
-
throw new Error(
|
|
55
|
-
'The i18n adapter loaded the React Router provider directly.',
|
|
56
|
-
);
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
write: false,
|
|
63
|
-
}),
|
|
64
|
-
).resolves.toBeDefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('shares Modern i18n context across independently bundled runtime copies', async () => {
|
|
68
|
-
const tempDir = await mkdtemp(
|
|
69
|
-
resolve(__dirname, '.modern-i18n-runtime-boundary-'),
|
|
70
|
-
);
|
|
71
|
-
try {
|
|
72
|
-
const contextEntry = resolve(runtimeRoot, 'context.tsx');
|
|
73
|
-
const copies = await Promise.all(
|
|
74
|
-
['copy-a.mjs', 'copy-b.mjs'].map(async filename => {
|
|
75
|
-
const outfile = resolve(tempDir, filename);
|
|
76
|
-
await build({
|
|
77
|
-
bundle: true,
|
|
78
|
-
entryPoints: [contextEntry],
|
|
79
|
-
format: 'esm',
|
|
80
|
-
jsx: 'automatic',
|
|
81
|
-
outfile,
|
|
82
|
-
packages: 'external',
|
|
83
|
-
platform: 'node',
|
|
84
|
-
});
|
|
85
|
-
return import(pathToFileURL(outfile).href);
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
const [copyA, copyB] = copies;
|
|
89
|
-
const instance = i18next.createInstance();
|
|
90
|
-
await instance.init({
|
|
91
|
-
initImmediate: false,
|
|
92
|
-
lng: 'cs',
|
|
93
|
-
resources: { cs: { translation: { language: 'Jazyk' } } },
|
|
94
|
-
});
|
|
95
|
-
const Consumer = () =>
|
|
96
|
-
createElement('span', null, copyB.useModernI18n().language);
|
|
97
|
-
|
|
98
|
-
const html = renderToStaticMarkup(
|
|
99
|
-
createElement(
|
|
100
|
-
copyA.ModernI18nProvider,
|
|
101
|
-
{
|
|
102
|
-
value: {
|
|
103
|
-
i18nInstance: instance,
|
|
104
|
-
language: 'cs',
|
|
105
|
-
languages: ['en', 'cs'],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
createElement(Consumer),
|
|
109
|
-
),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(html).toBe('<span>cs</span>');
|
|
113
|
-
} finally {
|
|
114
|
-
await rm(tempDir, { force: true, recursive: true });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@rstest/core';
|
|
2
|
-
import { shouldIgnoreRedirect as shouldIgnoreRuntimeRedirect } from '../src/runtime/utils';
|
|
3
|
-
import {
|
|
4
|
-
isStaticResourceRequest,
|
|
5
|
-
shouldIgnoreRedirect as shouldIgnoreServerRedirect,
|
|
6
|
-
} from '../src/server/redirectPolicy';
|
|
7
|
-
|
|
8
|
-
describe('native locale redirect safeguards', () => {
|
|
9
|
-
test('retains configured ignores across runtime and server entry prefixes', () => {
|
|
10
|
-
const rules = ['/private'];
|
|
11
|
-
expect(
|
|
12
|
-
shouldIgnoreRuntimeRedirect('/cs/private/data', ['en', 'cs'], rules),
|
|
13
|
-
).toBe(true);
|
|
14
|
-
expect(
|
|
15
|
-
shouldIgnoreRuntimeRedirect('/cs/private-data', ['en', 'cs'], rules),
|
|
16
|
-
).toBe(false);
|
|
17
|
-
expect(
|
|
18
|
-
shouldIgnoreServerRedirect(
|
|
19
|
-
'/app/cs/private/data',
|
|
20
|
-
'/app/*',
|
|
21
|
-
rules,
|
|
22
|
-
undefined,
|
|
23
|
-
['en', 'cs'],
|
|
24
|
-
),
|
|
25
|
-
).toBe(true);
|
|
26
|
-
expect(
|
|
27
|
-
shouldIgnoreServerRedirect(
|
|
28
|
-
'/app/cs/private-data',
|
|
29
|
-
'/app/*',
|
|
30
|
-
rules,
|
|
31
|
-
undefined,
|
|
32
|
-
['en', 'cs'],
|
|
33
|
-
),
|
|
34
|
-
).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
test('retains configured and native static paths without fork policy', () => {
|
|
37
|
-
for (const pathname of [
|
|
38
|
-
'/static/app.js',
|
|
39
|
-
'/upload/a.png',
|
|
40
|
-
'/cs/assets/main.js',
|
|
41
|
-
]) {
|
|
42
|
-
expect(isStaticResourceRequest(pathname, ['/assets'], ['en', 'cs'])).toBe(
|
|
43
|
-
true,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
expect(
|
|
47
|
-
isStaticResourceRequest(
|
|
48
|
-
'/assets-other/main.js',
|
|
49
|
-
['/assets'],
|
|
50
|
-
['en', 'cs'],
|
|
51
|
-
),
|
|
52
|
-
).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
InternalRuntimeContext,
|
|
3
|
-
RuntimeContext,
|
|
4
|
-
} from '@modern-js/runtime/context';
|
|
5
|
-
import type React from 'react';
|
|
6
|
-
import type { PropsWithChildren } from 'react';
|
|
7
|
-
import { act, useEffect } from 'react';
|
|
8
|
-
import { createRoot, type Root } from 'react-dom/client';
|
|
9
|
-
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
10
|
-
import type { I18nInstance } from '../src/runtime/i18n';
|
|
11
|
-
import { Link } from '../src/runtime/Link';
|
|
12
|
-
import {
|
|
13
|
-
createI18nRootWrapper,
|
|
14
|
-
type I18nLanguageSynchronizationProps,
|
|
15
|
-
} from '../src/runtime/providerComposition';
|
|
16
|
-
import {
|
|
17
|
-
I18nNavigationProvider,
|
|
18
|
-
useI18nRouterAdapter,
|
|
19
|
-
} from '../src/runtime/routerAdapter';
|
|
20
|
-
|
|
21
|
-
(
|
|
22
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
23
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
24
|
-
|
|
25
|
-
const requestContext = {
|
|
26
|
-
request: {},
|
|
27
|
-
response: {},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function createI18nInstance(language = 'en'): I18nInstance {
|
|
31
|
-
return {
|
|
32
|
-
language,
|
|
33
|
-
isInitialized: true,
|
|
34
|
-
init: () => Promise.resolve(undefined),
|
|
35
|
-
use: () => {},
|
|
36
|
-
t: (key: string | string[]) => (Array.isArray(key) ? key[0] : key),
|
|
37
|
-
createInstance: () => createI18nInstance(language),
|
|
38
|
-
setLang: rstest.fn(async () => undefined),
|
|
39
|
-
changeLanguage: rstest.fn(async () => undefined),
|
|
40
|
-
services: {},
|
|
41
|
-
options: {},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function renderI18nRoot(node: React.ReactNode) {
|
|
46
|
-
const container = document.createElement('div');
|
|
47
|
-
document.body.appendChild(container);
|
|
48
|
-
const root = createRoot(container);
|
|
49
|
-
|
|
50
|
-
await act(async () => {
|
|
51
|
-
root.render(
|
|
52
|
-
<RuntimeContext.Provider
|
|
53
|
-
value={{
|
|
54
|
-
isBrowser: true,
|
|
55
|
-
requestContext,
|
|
56
|
-
context: requestContext,
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
{node}
|
|
60
|
-
</RuntimeContext.Provider>,
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
container,
|
|
66
|
-
root,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function cleanup(rendered?: { container: HTMLElement; root: Root }) {
|
|
71
|
-
if (!rendered) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
act(() => {
|
|
75
|
-
rendered.root.unmount();
|
|
76
|
-
});
|
|
77
|
-
rendered.container.remove();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe('i18n runtime wrapRoot', () => {
|
|
81
|
-
let rendered: { container: HTMLElement; root: Root } | undefined;
|
|
82
|
-
|
|
83
|
-
afterEach(() => {
|
|
84
|
-
cleanup(rendered);
|
|
85
|
-
rendered = undefined;
|
|
86
|
-
rstest.useRealTimers();
|
|
87
|
-
rstest.restoreAllMocks();
|
|
88
|
-
window.history.replaceState(null, '', '/');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('keeps the optional i18next provider inside Modern i18n context', async () => {
|
|
92
|
-
const i18nInstance = createI18nInstance('cs');
|
|
93
|
-
const observedLanguages: string[] = [];
|
|
94
|
-
const I18nextProvider = ({
|
|
95
|
-
children,
|
|
96
|
-
i18n,
|
|
97
|
-
}: PropsWithChildren<{ i18n: I18nInstance }>) => {
|
|
98
|
-
const { language } = useModernI18n();
|
|
99
|
-
observedLanguages.push(`${language}:${i18n.language}`);
|
|
100
|
-
|
|
101
|
-
return <section data-testid="i18next-provider">{children}</section>;
|
|
102
|
-
};
|
|
103
|
-
const App = () => <main>router content</main>;
|
|
104
|
-
const I18nRoot = createI18nRootWrapper({
|
|
105
|
-
htmlLangAttr: false,
|
|
106
|
-
localePathRedirect: false,
|
|
107
|
-
languages: ['en', 'cs'],
|
|
108
|
-
fallbackLanguage: 'en',
|
|
109
|
-
getLatestI18nInstance: () => i18nInstance,
|
|
110
|
-
getI18nextProvider: () => I18nextProvider,
|
|
111
|
-
})(App);
|
|
112
|
-
|
|
113
|
-
rendered = await renderI18nRoot(<I18nRoot />);
|
|
114
|
-
|
|
115
|
-
expect(observedLanguages).toEqual(['cs:cs']);
|
|
116
|
-
expect(rendered.container.textContent).toContain('router content');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('native navigation extension contract', () => {
|
|
121
|
-
test('renders the supplied router primitive and exposes provider-scoped navigation', async () => {
|
|
122
|
-
const navigate = rstest.fn();
|
|
123
|
-
const RouterLink = ({ to, children }: any) => (
|
|
124
|
-
<a href={to} data-native="yes">
|
|
125
|
-
{children}
|
|
126
|
-
</a>
|
|
127
|
-
);
|
|
128
|
-
function Consumer() {
|
|
129
|
-
const adapter = useI18nRouterAdapter();
|
|
130
|
-
expect(adapter.navigate).toBe(navigate);
|
|
131
|
-
expect(adapter.params).toEqual({ lang: 'cs' });
|
|
132
|
-
return <Link to="/products?tag=x#detail">Products</Link>;
|
|
133
|
-
}
|
|
134
|
-
const rendered = await renderI18nRoot(
|
|
135
|
-
<I18nNavigationProvider
|
|
136
|
-
value={{
|
|
137
|
-
hasRouter: true,
|
|
138
|
-
navigate,
|
|
139
|
-
params: { lang: 'cs' },
|
|
140
|
-
location: { pathname: '/cs/products', search: '', hash: '' },
|
|
141
|
-
Link: RouterLink,
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
<ModernI18nProvider
|
|
145
|
-
value={{
|
|
146
|
-
language: 'cs',
|
|
147
|
-
i18nInstance: createI18nInstance('cs'),
|
|
148
|
-
languages: ['en', 'cs'],
|
|
149
|
-
localePathRedirect: true,
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
<Consumer />
|
|
153
|
-
</ModernI18nProvider>
|
|
154
|
-
</I18nNavigationProvider>,
|
|
155
|
-
);
|
|
156
|
-
try {
|
|
157
|
-
expect(rendered.container.querySelector('a')?.getAttribute('href')).toBe(
|
|
158
|
-
'/cs/products?tag=x#detail',
|
|
159
|
-
);
|
|
160
|
-
expect(
|
|
161
|
-
rendered.container.querySelector('a')?.getAttribute('data-native'),
|
|
162
|
-
).toBe('yes');
|
|
163
|
-
expect(navigate).not.toHaveBeenCalled();
|
|
164
|
-
} finally {
|
|
165
|
-
cleanup(rendered);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('native synchronization extension contract', () => {
|
|
171
|
-
test('uses the supplied component callback and preserves its state across parent renders', async () => {
|
|
172
|
-
const instance = createI18nInstance('en');
|
|
173
|
-
const synchronize = rstest.fn();
|
|
174
|
-
const mounts = rstest.fn();
|
|
175
|
-
const observed: I18nLanguageSynchronizationProps[] = [];
|
|
176
|
-
const Synchronization = (props: I18nLanguageSynchronizationProps) => {
|
|
177
|
-
observed.push(props);
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
mounts();
|
|
180
|
-
props.setLang('cs');
|
|
181
|
-
}, []);
|
|
182
|
-
return props.children(synchronize);
|
|
183
|
-
};
|
|
184
|
-
const Root = createI18nRootWrapper({
|
|
185
|
-
htmlLangAttr: false,
|
|
186
|
-
localePathRedirect: true,
|
|
187
|
-
languages: ['en', 'cs'],
|
|
188
|
-
fallbackLanguage: 'en',
|
|
189
|
-
getLatestI18nInstance: () => instance,
|
|
190
|
-
getI18nextProvider: () => null,
|
|
191
|
-
LanguageSynchronization: Synchronization,
|
|
192
|
-
})(() => {
|
|
193
|
-
const { language } = useModernI18n();
|
|
194
|
-
return <main>{language}</main>;
|
|
195
|
-
});
|
|
196
|
-
window.history.replaceState(null, '', '/cs/products');
|
|
197
|
-
const rendered = await renderI18nRoot(<Root />);
|
|
198
|
-
try {
|
|
199
|
-
expect(rendered.container.textContent).toBe('cs');
|
|
200
|
-
expect(synchronize).toHaveBeenCalledWith('cs');
|
|
201
|
-
expect(observed.at(-1)?.i18nInstance).toBe(instance);
|
|
202
|
-
expect(observed.at(-1)?.pathname).toBe('/cs/products');
|
|
203
|
-
expect(observed.at(-1)?.prevLangRef.current).toBe('cs');
|
|
204
|
-
await act(async () => {
|
|
205
|
-
rendered.root.render(
|
|
206
|
-
<RuntimeContext.Provider
|
|
207
|
-
value={{ isBrowser: true, requestContext, context: requestContext }}
|
|
208
|
-
>
|
|
209
|
-
<Root unrelated="updated" />
|
|
210
|
-
</RuntimeContext.Provider>,
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
expect(mounts).toHaveBeenCalledTimes(1);
|
|
214
|
-
expect(rendered.container.textContent).toBe('cs');
|
|
215
|
-
expect(instance.changeLanguage).not.toHaveBeenCalled();
|
|
216
|
-
} finally {
|
|
217
|
-
cleanup(rendered);
|
|
218
|
-
window.history.replaceState(null, '', '/');
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "@modern-js/tsconfig/base",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"declaration": false,
|
|
5
|
-
"lib": ["DOM", "ESNext", "dom.iterable"],
|
|
6
|
-
"jsx": "preserve",
|
|
7
|
-
// FORK: `baseUrl` removed. TypeScript 7 / tsgo reject it outright (TS5102),
|
|
8
|
-
// so this package's own tsconfig could not be typechecked at all. `paths`
|
|
9
|
-
// is empty here, so nothing resolved through baseUrl.
|
|
10
|
-
"isolatedModules": true,
|
|
11
|
-
"paths": {},
|
|
12
|
-
"rootDir": "./src"
|
|
13
|
-
},
|
|
14
|
-
"include": ["src", "types"]
|
|
15
|
-
}
|