@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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 +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
|
+
import { createAsyncHook, createPluginManager } from '@modern-js/plugin';
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
initAppContext,
|
|
6
|
+
initPluginAPI,
|
|
7
|
+
} from '@modern-js/plugin/cli';
|
|
8
|
+
import type { NestedRouteForCli } from '@modern-js/types';
|
|
9
|
+
import { describe, expect, test } from '@rstest/core';
|
|
10
|
+
import { type I18nPluginOptions, i18nPlugin } from '../src/cli';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Localised route generation must reach a bare `appTools()` consumer.
|
|
14
|
+
*
|
|
15
|
+
* The fork used to register it only from `@modern-js/i18n-integration`, which
|
|
16
|
+
* `ultramodernAppTools()` composes but plain `appTools()` does not — so an app
|
|
17
|
+
* declaring `localeDetection.localisedUrls` got a 404 on every mapped path
|
|
18
|
+
* under SSR. These cases deliberately register *only* the native plugin, with
|
|
19
|
+
* no integration plugin in the manager, so a regression back to the opt-in
|
|
20
|
+
* wiring fails here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const createRoute = (
|
|
24
|
+
path: string,
|
|
25
|
+
children?: NestedRouteForCli[],
|
|
26
|
+
): NestedRouteForCli => ({
|
|
27
|
+
id: path,
|
|
28
|
+
path,
|
|
29
|
+
type: 'nested',
|
|
30
|
+
origin: 'file-system',
|
|
31
|
+
routeType: children ? 'layout' : 'page',
|
|
32
|
+
_component: `${path}.tsx`,
|
|
33
|
+
children,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function createNativeOnlyHarness(options: I18nPluginOptions = {}) {
|
|
37
|
+
const manager = createPluginManager();
|
|
38
|
+
const routeHost: CliPlugin<AppTools> = {
|
|
39
|
+
name: 'test-route-host',
|
|
40
|
+
registryHooks: { modifyFileSystemRoutes: createAsyncHook<any>() },
|
|
41
|
+
setup() {
|
|
42
|
+
// Only supplies the hook registry the app-tools router owns.
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
manager.addPlugins([i18nPlugin(options), routeHost]);
|
|
47
|
+
const plugins = manager.getPlugins() as CliPlugin<AppTools>[];
|
|
48
|
+
const context = await createContext<AppTools>({
|
|
49
|
+
appContext: initAppContext({
|
|
50
|
+
packageName: 'plugin-i18n-native-only-test',
|
|
51
|
+
configFile: false,
|
|
52
|
+
command: 'build',
|
|
53
|
+
appDirectory: process.cwd(),
|
|
54
|
+
metaName: 'modern-js',
|
|
55
|
+
plugins,
|
|
56
|
+
}),
|
|
57
|
+
config: {},
|
|
58
|
+
normalizedConfig: {} as any,
|
|
59
|
+
});
|
|
60
|
+
const api = initPluginAPI<AppTools>({ context, pluginManager: manager });
|
|
61
|
+
for (const plugin of plugins) {
|
|
62
|
+
await plugin.setup?.(api);
|
|
63
|
+
}
|
|
64
|
+
return api.getHooks().modifyFileSystemRoutes.call;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entrypoint = { entryName: 'main' } as any;
|
|
68
|
+
|
|
69
|
+
const localisedUrls = {
|
|
70
|
+
'/about': { en: '/about', cs: '/o-nas' },
|
|
71
|
+
'/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe('native localised route generation', () => {
|
|
75
|
+
test('expands mapped locale paths without the integration plugin', async () => {
|
|
76
|
+
const modifyRoutes = await createNativeOnlyHarness({
|
|
77
|
+
localeDetection: {
|
|
78
|
+
localePathRedirect: true,
|
|
79
|
+
languages: ['en', 'cs'],
|
|
80
|
+
localisedUrls,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await modifyRoutes({
|
|
85
|
+
entrypoint,
|
|
86
|
+
routes: [
|
|
87
|
+
createRoute(':lang', [
|
|
88
|
+
createRoute('about'),
|
|
89
|
+
createRoute('products/:slug'),
|
|
90
|
+
]),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const paths = result.routes[0].children?.map(
|
|
95
|
+
(route: NestedRouteForCli) => route.path,
|
|
96
|
+
);
|
|
97
|
+
expect(paths).toContain('about');
|
|
98
|
+
expect(paths).toContain('o-nas');
|
|
99
|
+
expect(paths).toContain('products/:slug');
|
|
100
|
+
expect(paths).toContain('produkty/:slug');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('keeps one source identity for TanStack entries while validating locale mappings', async () => {
|
|
104
|
+
const modifyRoutes = await createNativeOnlyHarness({
|
|
105
|
+
localeDetection: {
|
|
106
|
+
localePathRedirect: true,
|
|
107
|
+
languages: ['en', 'cs'],
|
|
108
|
+
localisedUrls,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const result = await modifyRoutes({
|
|
112
|
+
entrypoint: {
|
|
113
|
+
...entrypoint,
|
|
114
|
+
__modernRoutesOwner: '@modern-js/plugin-tanstack',
|
|
115
|
+
},
|
|
116
|
+
routes: [
|
|
117
|
+
createRoute(':lang', [
|
|
118
|
+
createRoute('about'),
|
|
119
|
+
createRoute('products/:slug'),
|
|
120
|
+
]),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
expect(result.routes[0].children).toMatchObject([
|
|
124
|
+
{
|
|
125
|
+
id: 'about',
|
|
126
|
+
path: 'about',
|
|
127
|
+
modernLocalisedRoute: {
|
|
128
|
+
canonicalPath: '/about',
|
|
129
|
+
paths: localisedUrls['/about'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'products/:slug',
|
|
134
|
+
path: 'products/:slug',
|
|
135
|
+
modernLocalisedRoute: {
|
|
136
|
+
canonicalPath: '/products/:slug',
|
|
137
|
+
paths: localisedUrls['/products/:slug'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
expect(result.routes[0].children).toHaveLength(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('leaves routes untouched when no map is declared', async () => {
|
|
145
|
+
const modifyRoutes = await createNativeOnlyHarness({
|
|
146
|
+
localeDetection: {
|
|
147
|
+
localePathRedirect: true,
|
|
148
|
+
languages: ['en', 'cs'],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const input = [createRoute(':lang', [createRoute('about')])];
|
|
153
|
+
const result = await modifyRoutes({ entrypoint, routes: input });
|
|
154
|
+
|
|
155
|
+
expect(
|
|
156
|
+
result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
|
|
157
|
+
).toEqual(['about']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('leaves routes untouched without localePathRedirect', async () => {
|
|
161
|
+
const modifyRoutes = await createNativeOnlyHarness({
|
|
162
|
+
localeDetection: {
|
|
163
|
+
languages: ['en', 'cs'],
|
|
164
|
+
localisedUrls,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const input = [createRoute(':lang', [createRoute('about')])];
|
|
169
|
+
const result = await modifyRoutes({ entrypoint, routes: input });
|
|
170
|
+
|
|
171
|
+
expect(
|
|
172
|
+
result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
|
|
173
|
+
).toEqual(['about']);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('resolves the map from a per-entry locale detection override', async () => {
|
|
177
|
+
const modifyRoutes = await createNativeOnlyHarness({
|
|
178
|
+
localeDetection: {
|
|
179
|
+
localePathRedirect: true,
|
|
180
|
+
languages: ['en'],
|
|
181
|
+
localeDetectionByEntry: {
|
|
182
|
+
main: {
|
|
183
|
+
localePathRedirect: true,
|
|
184
|
+
languages: ['en', 'cs'],
|
|
185
|
+
localisedUrls,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await modifyRoutes({
|
|
192
|
+
entrypoint,
|
|
193
|
+
routes: [createRoute(':lang', [createRoute('about')])],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(
|
|
197
|
+
result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
|
|
198
|
+
).toContain('o-nas');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { NestedRouteForCli } from '@modern-js/types';
|
|
2
1
|
import { describe, expect, test } from '@rstest/core';
|
|
3
2
|
import { i18nPlugin as i18nCliPlugin } from '../src/cli';
|
|
4
3
|
import {
|
|
@@ -7,19 +6,6 @@ import {
|
|
|
7
6
|
matchesApiPrefix,
|
|
8
7
|
} from '../src/server';
|
|
9
8
|
|
|
10
|
-
const createRoute = (
|
|
11
|
-
path: string,
|
|
12
|
-
children?: NestedRouteForCli[],
|
|
13
|
-
): NestedRouteForCli => ({
|
|
14
|
-
id: path,
|
|
15
|
-
path,
|
|
16
|
-
type: 'nested',
|
|
17
|
-
origin: 'file-system',
|
|
18
|
-
routeType: children ? 'layout' : 'page',
|
|
19
|
-
_component: `${path}.tsx`,
|
|
20
|
-
children,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
9
|
const createRequestContext = (pathname: string) =>
|
|
24
10
|
({
|
|
25
11
|
req: {
|
|
@@ -63,149 +49,33 @@ describe('cli modifyFileSystemRoutes', () => {
|
|
|
63
49
|
expect(runtimePlugin?.config.reactI18next).toBe(false);
|
|
64
50
|
});
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
test('upstream-style configs without a map keep routes untouched', async () => {
|
|
53
|
+
// The hook is registered unconditionally now that route expansion is a
|
|
54
|
+
// native feature; what matters is that a config with no `localisedUrls`
|
|
55
|
+
// map gets its routes back byte-for-byte instead of a locale-expanded tree.
|
|
56
|
+
let handler:
|
|
57
|
+
| ((args: { entrypoint: unknown; routes: unknown[] }) => Promise<{
|
|
58
|
+
routes: unknown[];
|
|
59
|
+
}>)
|
|
72
60
|
| undefined;
|
|
73
61
|
|
|
74
|
-
i18nCliPlugin({
|
|
62
|
+
i18nCliPlugin({
|
|
63
|
+
localeDetection: { localePathRedirect: true, languages: ['en', 'cs'] },
|
|
64
|
+
}).setup({
|
|
75
65
|
_internalRuntimePlugins: () => {},
|
|
66
|
+
_internalServerPlugins: () => {},
|
|
76
67
|
modifyFileSystemRoutes: (fn: any) => {
|
|
77
|
-
|
|
68
|
+
handler = fn;
|
|
78
69
|
},
|
|
79
|
-
_internalServerPlugins: () => {},
|
|
80
70
|
} as any);
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const generateLocalisedRoutes = () => {
|
|
87
|
-
const modifyRoutes = setupModifyRoutes({
|
|
88
|
-
localePathRedirect: true,
|
|
89
|
-
languages: ['en', 'cs', 'de'],
|
|
90
|
-
localisedUrls: {
|
|
91
|
-
'/about': {
|
|
92
|
-
en: '/about',
|
|
93
|
-
cs: '/o-nas',
|
|
94
|
-
de: '/ueber-uns',
|
|
95
|
-
},
|
|
96
|
-
'/products': {
|
|
97
|
-
en: '/products',
|
|
98
|
-
cs: '/produkty',
|
|
99
|
-
de: '/produkte',
|
|
100
|
-
},
|
|
101
|
-
'/products/:slug': {
|
|
102
|
-
en: '/products/:slug',
|
|
103
|
-
cs: '/produkty/:slug',
|
|
104
|
-
de: '/produkte/:slug',
|
|
105
|
-
},
|
|
106
|
-
'/docs': {
|
|
107
|
-
en: '/docs',
|
|
108
|
-
cs: '/dokumenty',
|
|
109
|
-
de: '/dokumente',
|
|
110
|
-
},
|
|
111
|
-
'/docs/*': {
|
|
112
|
-
en: '/docs/*',
|
|
113
|
-
cs: '/dokumenty/*',
|
|
114
|
-
de: '/dokumente/*',
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
const routes = [
|
|
119
|
-
createRoute(':lang', [
|
|
120
|
-
createRoute('about'),
|
|
121
|
-
createRoute('products', [createRoute(':slug')]),
|
|
122
|
-
createRoute('docs', [createRoute('*')]),
|
|
123
|
-
]),
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
const result = modifyRoutes({
|
|
72
|
+
const routes = [{ id: 'about', path: 'about', type: 'nested' }];
|
|
73
|
+
const result = await handler?.({
|
|
127
74
|
entrypoint: { entryName: 'main' },
|
|
128
75
|
routes,
|
|
129
76
|
});
|
|
130
77
|
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
test('creates deterministic localized aliases with canonical route identity', () => {
|
|
135
|
-
const localisedRoutes = generateLocalisedRoutes();
|
|
136
|
-
expect(generateLocalisedRoutes()).toEqual(localisedRoutes);
|
|
137
|
-
|
|
138
|
-
const [localeLayout] = localisedRoutes;
|
|
139
|
-
expect(localeLayout.children?.map(route => route.path)).toEqual([
|
|
140
|
-
'about',
|
|
141
|
-
'o-nas',
|
|
142
|
-
'ueber-uns',
|
|
143
|
-
'products',
|
|
144
|
-
'produkty',
|
|
145
|
-
'produkte',
|
|
146
|
-
'docs',
|
|
147
|
-
'dokumenty',
|
|
148
|
-
'dokumente',
|
|
149
|
-
]);
|
|
150
|
-
expect(localeLayout.children?.slice(0, 3)).toMatchObject([
|
|
151
|
-
{
|
|
152
|
-
_component: 'about.tsx',
|
|
153
|
-
modernCanonicalPath: '/about',
|
|
154
|
-
path: 'about',
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
_component: 'about.tsx',
|
|
158
|
-
modernCanonicalPath: '/about',
|
|
159
|
-
path: 'o-nas',
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
_component: 'about.tsx',
|
|
163
|
-
modernCanonicalPath: '/about',
|
|
164
|
-
path: 'ueber-uns',
|
|
165
|
-
},
|
|
166
|
-
]);
|
|
167
|
-
for (const productRoute of localeLayout.children?.slice(3, 6) ?? []) {
|
|
168
|
-
expect(productRoute).toMatchObject({
|
|
169
|
-
_component: 'products.tsx',
|
|
170
|
-
modernCanonicalPath: '/products',
|
|
171
|
-
});
|
|
172
|
-
expect(productRoute.children?.[0]).toMatchObject({
|
|
173
|
-
_component: ':slug.tsx',
|
|
174
|
-
modernCanonicalPath: '/products/:slug',
|
|
175
|
-
path: ':slug',
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('upstream-style configs without a map keep routes untouched', () => {
|
|
181
|
-
const modifyRoutes = setupModifyRoutes({
|
|
182
|
-
localePathRedirect: true,
|
|
183
|
-
languages: ['en', 'cs'],
|
|
184
|
-
});
|
|
185
|
-
const routes = [createRoute(':lang', [createRoute('about')])];
|
|
186
|
-
|
|
187
|
-
const result = modifyRoutes({ entrypoint: { entryName: 'main' }, routes });
|
|
188
|
-
|
|
189
|
-
expect(result.routes).toBe(routes);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('a configured map still expands localised route aliases', () => {
|
|
193
|
-
const modifyRoutes = setupModifyRoutes({
|
|
194
|
-
localePathRedirect: true,
|
|
195
|
-
languages: ['en', 'cs'],
|
|
196
|
-
localisedUrls: {
|
|
197
|
-
'/about': { en: '/about', cs: '/o-nas' },
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
const routes = [createRoute(':lang', [createRoute('about')])];
|
|
201
|
-
|
|
202
|
-
const result = modifyRoutes({ entrypoint: { entryName: 'main' }, routes });
|
|
203
|
-
|
|
204
|
-
const localeRoute = result.routes[0] as NestedRouteForCli;
|
|
205
|
-
expect(localeRoute.children?.map(route => route.path)).toEqual([
|
|
206
|
-
'about',
|
|
207
|
-
'o-nas',
|
|
208
|
-
]);
|
|
78
|
+
expect(result?.routes).toEqual(routes);
|
|
209
79
|
});
|
|
210
80
|
});
|
|
211
81
|
|
|
@@ -283,68 +153,6 @@ describe('i18n server API prefix skips', () => {
|
|
|
283
153
|
}
|
|
284
154
|
});
|
|
285
155
|
|
|
286
|
-
test('canonical redirect survives malformed percent-encoding', async () => {
|
|
287
|
-
const middlewares: any[] = [];
|
|
288
|
-
const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
|
|
289
|
-
let prepare: (() => void) | undefined;
|
|
290
|
-
|
|
291
|
-
i18nServerPlugin({
|
|
292
|
-
localeDetection: {
|
|
293
|
-
fallbackLanguage: 'en',
|
|
294
|
-
languages: ['en', 'cs'],
|
|
295
|
-
localePathRedirect: true,
|
|
296
|
-
localisedUrls: {
|
|
297
|
-
'/products/:slug': {
|
|
298
|
-
en: '/products/:slug',
|
|
299
|
-
cs: '/produkty/:slug',
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
staticRoutePrefixes: [],
|
|
304
|
-
}).setup({
|
|
305
|
-
getServerConfig: () => ({}),
|
|
306
|
-
getServerContext: () => ({ middlewares, routes }),
|
|
307
|
-
onPrepare: fn => {
|
|
308
|
-
prepare = fn;
|
|
309
|
-
},
|
|
310
|
-
} as any);
|
|
311
|
-
|
|
312
|
-
prepare?.();
|
|
313
|
-
|
|
314
|
-
const redirectMiddleware = middlewares.find(
|
|
315
|
-
middleware => middleware.name === 'i18n-server-middleware',
|
|
316
|
-
);
|
|
317
|
-
const createContext = (pathname: string) =>
|
|
318
|
-
({
|
|
319
|
-
req: {
|
|
320
|
-
url: `http://localhost${pathname}`,
|
|
321
|
-
header: () => ({ host: 'localhost' }),
|
|
322
|
-
},
|
|
323
|
-
get: () => null,
|
|
324
|
-
}) as any;
|
|
325
|
-
|
|
326
|
-
// Sanity: well-formed non-canonical slugs still redirect.
|
|
327
|
-
const redirected = await redirectMiddleware.handler(
|
|
328
|
-
createContext('/cs/products/bota'),
|
|
329
|
-
async () => {},
|
|
330
|
-
);
|
|
331
|
-
expect(redirected.status).toBe(302);
|
|
332
|
-
expect(redirected.headers.get('location')).toBe('/cs/produkty/bota');
|
|
333
|
-
expect(redirected.headers.get('cache-control')).toBe('private, no-store');
|
|
334
|
-
expect(redirected.headers.get('vary')).toBe('Accept-Language, Cookie');
|
|
335
|
-
|
|
336
|
-
// Malformed encoding must fall through to next() instead of throwing.
|
|
337
|
-
let nextCalls = 0;
|
|
338
|
-
const response = await redirectMiddleware.handler(
|
|
339
|
-
createContext('/cs/produkty/%E0%A4%A'),
|
|
340
|
-
async () => {
|
|
341
|
-
nextCalls++;
|
|
342
|
-
},
|
|
343
|
-
);
|
|
344
|
-
expect(response).toBeUndefined();
|
|
345
|
-
expect(nextCalls).toBe(1);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
156
|
test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
|
|
349
157
|
const middlewares: any[] = [];
|
|
350
158
|
const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from '@rstest/core';
|
|
2
|
+
import { resolveMappedUrlStrategy } from '../src/shared/mappedUrlStrategy';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mapped locale URLs must work for a bare `appTools()` consumer that only
|
|
6
|
+
* declares `localeDetection.localisedUrls`. The map is plain data, so it
|
|
7
|
+
* survives the JSON boundary a `urlStrategy` object does not; the runtime
|
|
8
|
+
* derives its policy from it, and the server derives the same policy from the
|
|
9
|
+
* same shared module.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const localisedUrls = {
|
|
13
|
+
'/terms-of-service': { en: '/terms-of-service', cs: '/obchodni-podminky' },
|
|
14
|
+
'/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' },
|
|
15
|
+
};
|
|
16
|
+
const languages = ['en', 'cs'];
|
|
17
|
+
|
|
18
|
+
describe('mapped url strategy derivation', () => {
|
|
19
|
+
test('derives a localizing strategy from localisedUrls', () => {
|
|
20
|
+
const strategy = resolveMappedUrlStrategy({ localisedUrls });
|
|
21
|
+
|
|
22
|
+
expect(strategy).toBeDefined();
|
|
23
|
+
expect(
|
|
24
|
+
strategy?.localizePathname?.('/terms-of-service', 'cs', languages),
|
|
25
|
+
).toBe('/cs/obchodni-podminky');
|
|
26
|
+
expect(
|
|
27
|
+
strategy?.localizePathname?.('/terms-of-service', 'en', languages),
|
|
28
|
+
).toBe('/en/terms-of-service');
|
|
29
|
+
expect(
|
|
30
|
+
strategy?.canonicalPathname?.('/cs/obchodni-podminky', languages),
|
|
31
|
+
).toBe('/terms-of-service');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('round-trips a parameterised mapped route', () => {
|
|
35
|
+
const strategy = resolveMappedUrlStrategy({ localisedUrls });
|
|
36
|
+
const localized = strategy?.localizePathname?.(
|
|
37
|
+
'/products/widget',
|
|
38
|
+
'cs',
|
|
39
|
+
languages,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(localized).toBe('/cs/produkty/widget');
|
|
43
|
+
expect(strategy?.canonicalPathname?.(localized!, languages)).toBe(
|
|
44
|
+
'/products/widget',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('leaves unmapped paths on the plain language prefix', () => {
|
|
49
|
+
const strategy = resolveMappedUrlStrategy({ localisedUrls });
|
|
50
|
+
|
|
51
|
+
expect(strategy?.localizePathname?.('/unmapped', 'cs', languages)).toBe(
|
|
52
|
+
'/cs/unmapped',
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('derives nothing without a usable map', () => {
|
|
57
|
+
expect(resolveMappedUrlStrategy(undefined)).toBeUndefined();
|
|
58
|
+
expect(resolveMappedUrlStrategy({})).toBeUndefined();
|
|
59
|
+
expect(resolveMappedUrlStrategy({ localisedUrls: {} })).toBeUndefined();
|
|
60
|
+
expect(resolveMappedUrlStrategy({ localisedUrls: true })).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|