@bleedingdev/modern-js-i18n-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2
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/index.js +10 -10
- package/dist/cjs/localeRedirectPolicy.js +6 -6
- package/dist/cjs/native.js +160 -0
- package/dist/cjs/paths.js +11 -3
- package/dist/cjs/requestLanguage.js +195 -0
- package/dist/cjs/router-navigation/adapter.js +93 -0
- package/dist/cjs/{localisedUrls/config.js → router-navigation/index.js} +5 -14
- package/dist/cjs/ssrLanguageHandoff.js +94 -0
- package/dist/cjs/urlRewrite.js +64 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/localeRedirectPolicy.mjs +1 -1
- package/dist/esm/native.mjs +112 -0
- package/dist/esm/paths.mjs +7 -2
- package/dist/esm/requestLanguage.mjs +148 -0
- package/dist/esm/router-navigation/adapter.mjs +55 -0
- package/dist/esm/router-navigation/index.mjs +1 -0
- package/dist/esm/ssrLanguageHandoff.mjs +44 -0
- package/dist/esm/urlRewrite.mjs +26 -0
- package/dist/esm-node/index.mjs +1 -1
- package/dist/esm-node/localeRedirectPolicy.mjs +1 -1
- package/dist/esm-node/native.mjs +113 -0
- package/dist/esm-node/paths.mjs +7 -2
- package/dist/esm-node/requestLanguage.mjs +149 -0
- package/dist/esm-node/router-navigation/adapter.mjs +56 -0
- package/dist/esm-node/router-navigation/index.mjs +2 -0
- package/dist/esm-node/ssrLanguageHandoff.mjs +45 -0
- package/dist/esm-node/urlRewrite.mjs +27 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/localeRedirectPolicy.d.ts +1 -1
- package/dist/types/native.d.ts +21 -0
- package/dist/types/paths.d.ts +8 -1
- package/dist/types/requestLanguage.d.ts +60 -0
- package/dist/types/router-navigation/adapter.d.ts +23 -0
- package/dist/types/router-navigation/index.d.ts +1 -0
- package/dist/types/ssrLanguageHandoff.d.ts +60 -0
- package/dist/types/urlRewrite.d.ts +60 -0
- package/package.json +101 -14
- package/src/index.ts +1 -1
- package/src/localeRedirectPolicy.ts +2 -2
- package/src/native.ts +212 -0
- package/src/paths.ts +23 -2
- package/src/requestLanguage.ts +311 -0
- package/src/router-navigation/adapter.tsx +120 -0
- package/src/router-navigation/index.ts +6 -0
- package/src/ssrLanguageHandoff.ts +138 -0
- package/src/urlRewrite.ts +107 -0
- package/dist/cjs/localisedUrls/index.js +0 -98
- package/dist/cjs/localisedUrls/normalise.js +0 -87
- package/dist/cjs/localisedUrls/pathname.js +0 -68
- package/dist/cjs/localisedUrls/patterns.js +0 -214
- package/dist/cjs/localisedUrls/redirect.js +0 -100
- package/dist/cjs/localisedUrls/resolve.js +0 -78
- package/dist/cjs/localisedUrls/routeIdentity.js +0 -118
- package/dist/cjs/localisedUrls/routes.js +0 -210
- package/dist/cjs/localisedUrls/types.js +0 -18
- package/dist/esm/localisedUrls/config.mjs +0 -11
- package/dist/esm/localisedUrls/index.mjs +0 -8
- package/dist/esm/localisedUrls/normalise.mjs +0 -37
- package/dist/esm/localisedUrls/pathname.mjs +0 -24
- package/dist/esm/localisedUrls/patterns.mjs +0 -152
- package/dist/esm/localisedUrls/redirect.mjs +0 -53
- package/dist/esm/localisedUrls/resolve.mjs +0 -37
- package/dist/esm/localisedUrls/routeIdentity.mjs +0 -77
- package/dist/esm/localisedUrls/routes.mjs +0 -169
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm-node/localisedUrls/config.mjs +0 -12
- package/dist/esm-node/localisedUrls/index.mjs +0 -9
- package/dist/esm-node/localisedUrls/normalise.mjs +0 -38
- package/dist/esm-node/localisedUrls/pathname.mjs +0 -25
- package/dist/esm-node/localisedUrls/patterns.mjs +0 -153
- package/dist/esm-node/localisedUrls/redirect.mjs +0 -54
- package/dist/esm-node/localisedUrls/resolve.mjs +0 -38
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +0 -78
- package/dist/esm-node/localisedUrls/routes.mjs +0 -170
- package/dist/esm-node/localisedUrls/types.mjs +0 -1
- package/dist/types/localisedUrls/config.d.ts +0 -9
- package/dist/types/localisedUrls/index.d.ts +0 -10
- package/dist/types/localisedUrls/normalise.d.ts +0 -10
- package/dist/types/localisedUrls/pathname.d.ts +0 -4
- package/dist/types/localisedUrls/patterns.d.ts +0 -11
- package/dist/types/localisedUrls/redirect.d.ts +0 -19
- package/dist/types/localisedUrls/resolve.d.ts +0 -7
- package/dist/types/localisedUrls/routeIdentity.d.ts +0 -24
- package/dist/types/localisedUrls/routes.d.ts +0 -3
- package/dist/types/localisedUrls/types.d.ts +0 -14
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -36
- package/src/localisedUrls/config.ts +0 -18
- package/src/localisedUrls/index.ts +0 -28
- package/src/localisedUrls/normalise.ts +0 -62
- package/src/localisedUrls/pathname.ts +0 -60
- package/src/localisedUrls/patterns.ts +0 -283
- package/src/localisedUrls/redirect.ts +0 -107
- package/src/localisedUrls/resolve.ts +0 -106
- package/src/localisedUrls/routeIdentity.ts +0 -107
- package/src/localisedUrls/routes.ts +0 -432
- package/src/localisedUrls/types.ts +0 -18
- package/tests/languageSyncController.test.ts +0 -136
- package/tests/localeRedirectPolicy.test.ts +0 -173
- package/tests/localisedUrls.test.ts +0 -532
- package/tests/paths.test.ts +0 -24
- package/tests/reactLanguageSync.test.tsx +0 -136
- package/tests/tsconfig.json +0 -11
- package/tsconfig.json +0 -11
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@rstest/core';
|
|
2
|
-
import {
|
|
3
|
-
buildLocalizedUrl,
|
|
4
|
-
createLocaleRedirectResponse,
|
|
5
|
-
getLanguageFromPath,
|
|
6
|
-
isStaticResourceRequest,
|
|
7
|
-
shouldIgnoreRedirect,
|
|
8
|
-
} from '../src/localeRedirectPolicy';
|
|
9
|
-
|
|
10
|
-
const languages = ['en', 'cs'];
|
|
11
|
-
const request = (url: string) => ({
|
|
12
|
-
url,
|
|
13
|
-
header: () => ({ host: 'example.com' }),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('locale redirect resource policy', () => {
|
|
17
|
-
test('exempts default static and federation routes, including locale prefixes', () => {
|
|
18
|
-
const paths = [
|
|
19
|
-
'/backend-mf-manifest.json',
|
|
20
|
-
'/backendRemoteEntry.cjs',
|
|
21
|
-
'/mf-manifest.json',
|
|
22
|
-
'/mf-stats.json',
|
|
23
|
-
'/remoteEntry.js',
|
|
24
|
-
// A manifest may name a root entry; the federation layer serves these.
|
|
25
|
-
'/remoteEntry.catalog.js',
|
|
26
|
-
'/backendRemoteEntry.catalog-v2.cjs',
|
|
27
|
-
'/static/app.js',
|
|
28
|
-
'/upload/avatar.png',
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
for (const pathname of paths) {
|
|
32
|
-
expect(isStaticResourceRequest(pathname, [], languages)).toBe(true);
|
|
33
|
-
expect(isStaticResourceRequest(`/cs${pathname}`, [], languages)).toBe(
|
|
34
|
-
true,
|
|
35
|
-
);
|
|
36
|
-
expect(shouldIgnoreRedirect(`/store${pathname}`, '/store/*')).toBe(true);
|
|
37
|
-
}
|
|
38
|
-
// Only the entry itself, not a page or asset that merely starts like one.
|
|
39
|
-
for (const pathname of [
|
|
40
|
-
'/remoteEntry.catalog.js.map',
|
|
41
|
-
'/remoteEntry.catalog/js',
|
|
42
|
-
'/products/remoteEntry.js',
|
|
43
|
-
]) {
|
|
44
|
-
expect(isStaticResourceRequest(pathname, [], languages)).toBe(false);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('configured static prefixes match complete path segments with or without a locale', () => {
|
|
49
|
-
for (const pathname of ['/assets', '/assets/app.js', '/en/assets/app.js']) {
|
|
50
|
-
expect(isStaticResourceRequest(pathname, ['/assets/'], languages)).toBe(
|
|
51
|
-
true,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
for (const pathname of [
|
|
55
|
-
'/assets-extra/app.js',
|
|
56
|
-
'/cs/assets-extra/app.js',
|
|
57
|
-
]) {
|
|
58
|
-
expect(isStaticResourceRequest(pathname, ['/assets'], languages)).toBe(
|
|
59
|
-
false,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
expect(
|
|
63
|
-
isStaticResourceRequest('/fr/assets/app.js', ['/assets'], languages),
|
|
64
|
-
).toBe(false);
|
|
65
|
-
expect(
|
|
66
|
-
isStaticResourceRequest('/remoteEntry.js/product', [], languages),
|
|
67
|
-
).toBe(false);
|
|
68
|
-
expect(isStaticResourceRequest('/static-product', [])).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('custom ignore lists operate after stripping the mount path', () => {
|
|
72
|
-
expect(
|
|
73
|
-
shouldIgnoreRedirect('/store/health/live', '/store/*', ['/health']),
|
|
74
|
-
).toBe(true);
|
|
75
|
-
expect(
|
|
76
|
-
shouldIgnoreRedirect('/store/healthy', '/store/*', ['/health']),
|
|
77
|
-
).toBe(false);
|
|
78
|
-
expect(shouldIgnoreRedirect('/store/products', '/store/*')).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('custom callbacks receive the remaining pathname and default exemptions take priority', () => {
|
|
82
|
-
const seen: string[] = [];
|
|
83
|
-
const ignore = (pathname: string) => {
|
|
84
|
-
seen.push(pathname);
|
|
85
|
-
return pathname === '/health';
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
expect(shouldIgnoreRedirect('/store/health', '/store/*', ignore)).toBe(
|
|
89
|
-
true,
|
|
90
|
-
);
|
|
91
|
-
expect(shouldIgnoreRedirect('/store/products', '/store/*', ignore)).toBe(
|
|
92
|
-
false,
|
|
93
|
-
);
|
|
94
|
-
expect(
|
|
95
|
-
shouldIgnoreRedirect('/store/static/app.js', '/store/*', ignore),
|
|
96
|
-
).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('locale redirect URL policy', () => {
|
|
101
|
-
test('detects a configured first language segment for relative or absolute URLs', () => {
|
|
102
|
-
expect(
|
|
103
|
-
getLanguageFromPath(
|
|
104
|
-
request('/store/cs/products?x=1'),
|
|
105
|
-
'/store/*',
|
|
106
|
-
languages,
|
|
107
|
-
),
|
|
108
|
-
).toBe('cs');
|
|
109
|
-
expect(
|
|
110
|
-
getLanguageFromPath(
|
|
111
|
-
request('https://example.com/en/products'),
|
|
112
|
-
'/',
|
|
113
|
-
languages,
|
|
114
|
-
),
|
|
115
|
-
).toBe('en');
|
|
116
|
-
expect(getLanguageFromPath(request('/cs/products'), '', languages)).toBe(
|
|
117
|
-
'cs',
|
|
118
|
-
);
|
|
119
|
-
expect(
|
|
120
|
-
getLanguageFromPath(request('/storecs/products'), '/store/*', languages),
|
|
121
|
-
).toBe('cs');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('switches localized dynamic paths while preserving mount, query and hash', () => {
|
|
125
|
-
expect(
|
|
126
|
-
buildLocalizedUrl(
|
|
127
|
-
request(
|
|
128
|
-
'https://example.com/store/en/products/red%20tractor?q=a%2Fb&sort=price#details',
|
|
129
|
-
),
|
|
130
|
-
'/store/*',
|
|
131
|
-
'cs',
|
|
132
|
-
languages,
|
|
133
|
-
{ '/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' } },
|
|
134
|
-
),
|
|
135
|
-
).toBe('/store/cs/produkty/red%20tractor?q=a%2Fb&sort=price#details');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('supports root and empty mounts and missing locale mapping', () => {
|
|
139
|
-
for (const mount of ['/', '', '/*']) {
|
|
140
|
-
expect(
|
|
141
|
-
buildLocalizedUrl(
|
|
142
|
-
request('https://example.com/products?x=1#top'),
|
|
143
|
-
mount,
|
|
144
|
-
'cs',
|
|
145
|
-
languages,
|
|
146
|
-
),
|
|
147
|
-
).toBe('/cs/products?x=1#top');
|
|
148
|
-
}
|
|
149
|
-
expect(
|
|
150
|
-
buildLocalizedUrl(
|
|
151
|
-
request('https://example.com/store'),
|
|
152
|
-
'/store/*',
|
|
153
|
-
'cs',
|
|
154
|
-
languages,
|
|
155
|
-
false,
|
|
156
|
-
),
|
|
157
|
-
).toBe('/store/cs');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('returns a non-cacheable temporary redirect with language negotiation headers', async () => {
|
|
161
|
-
const response = createLocaleRedirectResponse(
|
|
162
|
-
'/cs/produkty?sort=price#details',
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
expect(response.status).toBe(302);
|
|
166
|
-
expect(response.headers.get('Location')).toBe(
|
|
167
|
-
'/cs/produkty?sort=price#details',
|
|
168
|
-
);
|
|
169
|
-
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
|
|
170
|
-
expect(response.headers.get('Vary')).toBe('Accept-Language, Cookie');
|
|
171
|
-
expect(await response.text()).toBe('');
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@rstest/core';
|
|
2
|
-
import type { LocalisedRoute } from '../src/localisedUrls/index';
|
|
3
|
-
import {
|
|
4
|
-
applyLocalisedUrlsToRoutes,
|
|
5
|
-
canonicalTargetPathname,
|
|
6
|
-
localiseTargetPathname,
|
|
7
|
-
matchPathPattern,
|
|
8
|
-
resolveCanonicalLocalisedPath,
|
|
9
|
-
resolveLocalisedPath,
|
|
10
|
-
validateLocalisedUrls,
|
|
11
|
-
} from '../src/localisedUrls/index';
|
|
12
|
-
|
|
13
|
-
const createRoute = (
|
|
14
|
-
path: string,
|
|
15
|
-
children?: LocalisedRoute[],
|
|
16
|
-
): LocalisedRoute => ({
|
|
17
|
-
id: path,
|
|
18
|
-
path,
|
|
19
|
-
type: 'nested',
|
|
20
|
-
origin: 'file-system',
|
|
21
|
-
routeType: children ? 'layout' : 'page',
|
|
22
|
-
_component: `${path}.tsx`,
|
|
23
|
-
children,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('localisedUrls', () => {
|
|
27
|
-
test('leaves framework-internal Module Federation routes unlocalised', () => {
|
|
28
|
-
const internalRoute = createRoute('_mf', [
|
|
29
|
-
createRoute('fragment', [createRoute('product-card')]),
|
|
30
|
-
]);
|
|
31
|
-
const publicRoute = createRoute(':lang', [createRoute('products')]);
|
|
32
|
-
const routes = [internalRoute, publicRoute];
|
|
33
|
-
|
|
34
|
-
expect(() =>
|
|
35
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
36
|
-
'/products': { en: '/products', cs: '/produkty' },
|
|
37
|
-
}),
|
|
38
|
-
).not.toThrow();
|
|
39
|
-
expect(
|
|
40
|
-
applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
41
|
-
'/products': { en: '/products', cs: '/produkty' },
|
|
42
|
-
})[0],
|
|
43
|
-
).toEqual(internalRoute);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('requires every localisable route path to define every language', () => {
|
|
47
|
-
const routes = [createRoute(':lang', [createRoute('terms-of-service')])];
|
|
48
|
-
|
|
49
|
-
expect(() =>
|
|
50
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
51
|
-
'/terms-of-service': {
|
|
52
|
-
en: '/terms-of-service',
|
|
53
|
-
},
|
|
54
|
-
}),
|
|
55
|
-
).toThrow('missing languages: cs');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('requires canonical and localised patterns to keep the same parameter signature', () => {
|
|
59
|
-
const routes = [createRoute('/:lang/products/:slug')];
|
|
60
|
-
|
|
61
|
-
expect(() =>
|
|
62
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
63
|
-
'/products/:slug': {
|
|
64
|
-
en: '/products/:slug',
|
|
65
|
-
cs: '/produkty/:produkt',
|
|
66
|
-
},
|
|
67
|
-
}),
|
|
68
|
-
).toThrow(
|
|
69
|
-
'localisedUrls["/products/:slug"].cs must use the same parameter signature',
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('allows locales to reorder the same named parameters', () => {
|
|
74
|
-
const routes = [createRoute('/:lang/catalog/:category/:slug')];
|
|
75
|
-
const localisedUrls = {
|
|
76
|
-
'/catalog/:category/:slug': {
|
|
77
|
-
en: '/catalog/:category/:slug',
|
|
78
|
-
cs: '/katalog/:slug/kategorie/:category',
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
expect(() =>
|
|
83
|
-
validateLocalisedUrls(routes, ['en', 'cs'], localisedUrls),
|
|
84
|
-
).not.toThrow();
|
|
85
|
-
expect(
|
|
86
|
-
resolveLocalisedPath(
|
|
87
|
-
'/catalog/shoes/red',
|
|
88
|
-
'cs',
|
|
89
|
-
['en', 'cs'],
|
|
90
|
-
localisedUrls,
|
|
91
|
-
),
|
|
92
|
-
).toBe('/katalog/red/kategorie/shoes');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('rejects reordering optional parameters whose position affects matching', () => {
|
|
96
|
-
const routes = [createRoute('/:lang/catalog/:category?/:slug?')];
|
|
97
|
-
|
|
98
|
-
expect(() =>
|
|
99
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
100
|
-
'/catalog/:category?/:slug?': {
|
|
101
|
-
en: '/catalog/:category?/:slug?',
|
|
102
|
-
cs: '/katalog/:slug?/:category?',
|
|
103
|
-
},
|
|
104
|
-
}),
|
|
105
|
-
).toThrow('must keep optional and splat parameters in canonical order');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('allows languages of one canonical route to share a physical alias', () => {
|
|
109
|
-
const routes = [createRoute('/:lang/about')];
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
113
|
-
'/about': {
|
|
114
|
-
en: '/about',
|
|
115
|
-
cs: '/about',
|
|
116
|
-
},
|
|
117
|
-
}).map(route => route.path),
|
|
118
|
-
).toEqual([':lang/about']);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('rejects distinct canonical routes that generate the same physical path', () => {
|
|
122
|
-
const routes = [
|
|
123
|
-
createRoute('/:lang/products/:slug'),
|
|
124
|
-
createRoute('/:lang/articles/:article'),
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
expect(() =>
|
|
128
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
129
|
-
'/products/:slug': {
|
|
130
|
-
en: '/catalog/:slug',
|
|
131
|
-
cs: '/produkty/:slug',
|
|
132
|
-
},
|
|
133
|
-
'/articles/:article': {
|
|
134
|
-
en: '/catalog/:article',
|
|
135
|
-
cs: '/clanky/:article',
|
|
136
|
-
},
|
|
137
|
-
}),
|
|
138
|
-
).toThrow('generate the same physical route pattern');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('rejects an alias owned by another route canonical pattern', () => {
|
|
142
|
-
const routes = [createRoute('/:lang/about'), createRoute('/:lang/company')];
|
|
143
|
-
|
|
144
|
-
expect(() =>
|
|
145
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
146
|
-
'/about': {
|
|
147
|
-
en: '/about-en',
|
|
148
|
-
cs: '/about-cs',
|
|
149
|
-
},
|
|
150
|
-
'/company': {
|
|
151
|
-
en: '/company-en',
|
|
152
|
-
cs: '/about',
|
|
153
|
-
},
|
|
154
|
-
}),
|
|
155
|
-
).toThrow('generate the same physical route pattern "/about"');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('rejects equal-specificity aliases that can match the same path', () => {
|
|
159
|
-
const routes = [
|
|
160
|
-
createRoute('/:lang/alpha/:slug'),
|
|
161
|
-
createRoute('/:lang/beta/:section'),
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
expect(() =>
|
|
165
|
-
validateLocalisedUrls(routes, ['en', 'cs'], {
|
|
166
|
-
'/alpha/:slug': {
|
|
167
|
-
en: '/x/:slug',
|
|
168
|
-
cs: '/alfa/:slug',
|
|
169
|
-
},
|
|
170
|
-
'/beta/:section': {
|
|
171
|
-
en: '/:section/y',
|
|
172
|
-
cs: '/beta/:section',
|
|
173
|
-
},
|
|
174
|
-
}),
|
|
175
|
-
).toThrow('overlapping route patterns');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('disambiguates lossy route ID suffixes without rejecting valid URLs', () => {
|
|
179
|
-
const routes = [createRoute('/:lang/about')];
|
|
180
|
-
const localisedUrls = {
|
|
181
|
-
'/about': {
|
|
182
|
-
en: '/about',
|
|
183
|
-
cs: '/o.nas',
|
|
184
|
-
de: '/o_nas',
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const localisedRoutes = applyLocalisedUrlsToRoutes(
|
|
189
|
-
routes,
|
|
190
|
-
['en', 'cs', 'de'],
|
|
191
|
-
localisedUrls,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
expect(localisedRoutes.map(route => route.path)).toEqual([
|
|
195
|
-
':lang/about',
|
|
196
|
-
':lang/o.nas',
|
|
197
|
-
':lang/o_nas',
|
|
198
|
-
]);
|
|
199
|
-
expect(new Set(localisedRoutes.map(route => route.id)).size).toBe(3);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test('rejects a generated route ID that collides with an existing route', () => {
|
|
203
|
-
const collidingRoute = {
|
|
204
|
-
...createRoute('_mf/health'),
|
|
205
|
-
id: '/:lang/about__localised_lang_o_nas',
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
expect(() =>
|
|
209
|
-
applyLocalisedUrlsToRoutes(
|
|
210
|
-
[createRoute('/:lang/about'), collidingRoute],
|
|
211
|
-
['en', 'cs'],
|
|
212
|
-
{
|
|
213
|
-
'/about': {
|
|
214
|
-
en: '/about',
|
|
215
|
-
cs: '/o.nas',
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
),
|
|
219
|
-
).toThrow('generated duplicate route ID');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('rejects duplicate params and non-terminal splats', () => {
|
|
223
|
-
expect(() =>
|
|
224
|
-
validateLocalisedUrls(
|
|
225
|
-
[createRoute('/:lang/pairs/:id/:id')],
|
|
226
|
-
['en', 'cs'],
|
|
227
|
-
{
|
|
228
|
-
'/pairs/:id/:id': {
|
|
229
|
-
en: '/pairs/:id/:id',
|
|
230
|
-
cs: '/dvojice/:id/:id',
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
),
|
|
234
|
-
).toThrow('duplicate path parameter ":id"');
|
|
235
|
-
|
|
236
|
-
expect(() =>
|
|
237
|
-
validateLocalisedUrls([createRoute('/:lang/docs/*/edit')], ['en', 'cs'], {
|
|
238
|
-
'/docs/*/edit': {
|
|
239
|
-
en: '/docs/*/edit',
|
|
240
|
-
cs: '/dokumenty/*/upravit',
|
|
241
|
-
},
|
|
242
|
-
}),
|
|
243
|
-
).toThrow('splat parameter "*" must be the final segment');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('expands route paths to localised aliases', () => {
|
|
247
|
-
const routes = [
|
|
248
|
-
createRoute(':lang', [
|
|
249
|
-
createRoute('terms-of-service'),
|
|
250
|
-
createRoute('products', [createRoute(':slug')]),
|
|
251
|
-
]),
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
|
|
255
|
-
'/terms-of-service': {
|
|
256
|
-
en: '/terms-of-service',
|
|
257
|
-
cs: '/podminky-pouzivani',
|
|
258
|
-
},
|
|
259
|
-
'/products': {
|
|
260
|
-
en: '/products',
|
|
261
|
-
cs: '/produkty',
|
|
262
|
-
},
|
|
263
|
-
'/products/:slug': {
|
|
264
|
-
en: '/products/:slug',
|
|
265
|
-
cs: '/produkty/:slug',
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const localeRoute = localisedRoutes[0] as LocalisedRoute;
|
|
270
|
-
expect(localeRoute.children?.map(route => route.path)).toEqual([
|
|
271
|
-
'terms-of-service',
|
|
272
|
-
'podminky-pouzivani',
|
|
273
|
-
'products',
|
|
274
|
-
'produkty',
|
|
275
|
-
]);
|
|
276
|
-
|
|
277
|
-
const productRoutes = localeRoute.children?.filter(
|
|
278
|
-
route => route.path === 'products' || route.path === 'produkty',
|
|
279
|
-
);
|
|
280
|
-
expect(productRoutes?.[0].children?.map(route => route.path)).toEqual([
|
|
281
|
-
':slug',
|
|
282
|
-
]);
|
|
283
|
-
expect(productRoutes?.[1].children?.map(route => route.path)).toEqual([
|
|
284
|
-
':slug',
|
|
285
|
-
]);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test('resolves optional route params', () => {
|
|
289
|
-
const localisedUrls = {
|
|
290
|
-
'/products/:slug?': {
|
|
291
|
-
en: '/products/:slug?',
|
|
292
|
-
cs: '/produkty/:slug?',
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
resolveLocalisedPath('/products', 'cs', ['en', 'cs'], localisedUrls),
|
|
298
|
-
).toBe('/produkty');
|
|
299
|
-
expect(
|
|
300
|
-
resolveLocalisedPath(
|
|
301
|
-
'/produkty/cervena-bota',
|
|
302
|
-
'en',
|
|
303
|
-
['en', 'cs'],
|
|
304
|
-
localisedUrls,
|
|
305
|
-
),
|
|
306
|
-
).toBe('/products/cervena-bota');
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test('empty splats round-trip and exact static routes keep precedence', () => {
|
|
310
|
-
expect(matchPathPattern('/docs', '/docs/*')).toEqual({ '*': '' });
|
|
311
|
-
expect(
|
|
312
|
-
resolveLocalisedPath('/docs', 'cs', ['en', 'cs'], {
|
|
313
|
-
'/docs/*': {
|
|
314
|
-
en: '/docs/*',
|
|
315
|
-
cs: '/dokumenty/*',
|
|
316
|
-
},
|
|
317
|
-
}),
|
|
318
|
-
).toBe('/dokumenty');
|
|
319
|
-
|
|
320
|
-
expect(
|
|
321
|
-
resolveLocalisedPath('/docs', 'cs', ['en', 'cs'], {
|
|
322
|
-
'/docs/*': {
|
|
323
|
-
en: '/docs/*',
|
|
324
|
-
cs: '/dokumenty/*',
|
|
325
|
-
},
|
|
326
|
-
'/docs': {
|
|
327
|
-
en: '/docs',
|
|
328
|
-
cs: '/dokumenty-domu',
|
|
329
|
-
},
|
|
330
|
-
}),
|
|
331
|
-
).toBe('/dokumenty-domu');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test('encodes wildcard params while preserving path separators', () => {
|
|
335
|
-
const localisedUrls = {
|
|
336
|
-
'/docs/*': {
|
|
337
|
-
en: '/docs/*',
|
|
338
|
-
cs: '/dokumenty/*',
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
expect(
|
|
343
|
-
resolveLocalisedPath(
|
|
344
|
-
'/docs/a%20b/c%23d',
|
|
345
|
-
'cs',
|
|
346
|
-
['en', 'cs'],
|
|
347
|
-
localisedUrls,
|
|
348
|
-
),
|
|
349
|
-
).toBe('/dokumenty/a%20b/c%23d');
|
|
350
|
-
expect(
|
|
351
|
-
resolveCanonicalLocalisedPath(
|
|
352
|
-
'/dokumenty/a%20b/c%23d',
|
|
353
|
-
['en', 'cs'],
|
|
354
|
-
localisedUrls,
|
|
355
|
-
),
|
|
356
|
-
).toBe('/docs/a%20b/c%23d');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test('resolves static patterns before param patterns', () => {
|
|
360
|
-
const localisedUrls = {
|
|
361
|
-
'/products/:slug': {
|
|
362
|
-
en: '/products/:slug',
|
|
363
|
-
cs: '/produkty/:slug',
|
|
364
|
-
},
|
|
365
|
-
'/products/new': {
|
|
366
|
-
en: '/products/new',
|
|
367
|
-
cs: '/produkty/novinka',
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
expect(
|
|
372
|
-
resolveLocalisedPath('/products/new', 'cs', ['en', 'cs'], localisedUrls),
|
|
373
|
-
).toBe('/produkty/novinka');
|
|
374
|
-
expect(
|
|
375
|
-
resolveCanonicalLocalisedPath(
|
|
376
|
-
'/produkty/novinka',
|
|
377
|
-
['en', 'cs'],
|
|
378
|
-
localisedUrls,
|
|
379
|
-
),
|
|
380
|
-
).toBe('/products/new');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('reverse resolution prefers the most specific localised source pattern', () => {
|
|
384
|
-
const localisedUrls = {
|
|
385
|
-
'/catalog/:section/:item/:detail': {
|
|
386
|
-
en: '/:section/:item/:detail',
|
|
387
|
-
cs: '/katalog/:section/:item/:detail',
|
|
388
|
-
},
|
|
389
|
-
'/:category/:slug': {
|
|
390
|
-
en: '/store/:category/:slug',
|
|
391
|
-
cs: '/obchod/:category/:slug',
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
expect(
|
|
396
|
-
resolveCanonicalLocalisedPath(
|
|
397
|
-
'/store/shoes/red',
|
|
398
|
-
['en', 'cs'],
|
|
399
|
-
localisedUrls,
|
|
400
|
-
),
|
|
401
|
-
).toBe('/shoes/red');
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
test('missing required target params never collapse into empty segments', () => {
|
|
405
|
-
expect(() =>
|
|
406
|
-
resolveLocalisedPath('/products/red-shoe', 'cs', ['en', 'cs'], {
|
|
407
|
-
'/products/:slug': {
|
|
408
|
-
en: '/products/:slug',
|
|
409
|
-
cs: '/produkty/:product',
|
|
410
|
-
},
|
|
411
|
-
}),
|
|
412
|
-
).toThrow('Missing required path parameter "product"');
|
|
413
|
-
|
|
414
|
-
expect(() =>
|
|
415
|
-
resolveLocalisedPath('/products/red-shoe', 'cs', ['en', 'cs'], {
|
|
416
|
-
'/products/:slug': {
|
|
417
|
-
en: '/products/:slug',
|
|
418
|
-
cs: '/produkty/:toString',
|
|
419
|
-
},
|
|
420
|
-
}),
|
|
421
|
-
).toThrow('Missing required path parameter "toString"');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test('strips case-insensitive locale prefixes before relocalising full pathnames', () => {
|
|
425
|
-
const localisedUrls = {
|
|
426
|
-
'/products/:slug': {
|
|
427
|
-
en: '/products/:slug',
|
|
428
|
-
cs: '/produkty/:slug',
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
expect(
|
|
433
|
-
localiseTargetPathname(
|
|
434
|
-
'/CS/produkty/cervena-bota',
|
|
435
|
-
'en',
|
|
436
|
-
['en', 'cs'],
|
|
437
|
-
localisedUrls,
|
|
438
|
-
),
|
|
439
|
-
).toBe('/en/products/cervena-bota');
|
|
440
|
-
expect(
|
|
441
|
-
canonicalTargetPathname(
|
|
442
|
-
'/CS/produkty/cervena-bota',
|
|
443
|
-
['en', 'cs'],
|
|
444
|
-
localisedUrls,
|
|
445
|
-
),
|
|
446
|
-
).toBe('/products/cervena-bota');
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test('resolves nested optional route params with translated ancestors', () => {
|
|
450
|
-
const localisedUrls = {
|
|
451
|
-
'/checkout': {
|
|
452
|
-
en: '/checkout',
|
|
453
|
-
cs: '/pokladna',
|
|
454
|
-
},
|
|
455
|
-
'/checkout/thank-you': {
|
|
456
|
-
en: '/checkout/thank-you',
|
|
457
|
-
cs: '/pokladna/dekujeme',
|
|
458
|
-
},
|
|
459
|
-
'/checkout/thank-you/:orderId?': {
|
|
460
|
-
en: '/checkout/thank-you/:orderId?',
|
|
461
|
-
cs: '/pokladna/dekujeme/:orderId?',
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
expect(
|
|
466
|
-
resolveLocalisedPath(
|
|
467
|
-
'/checkout/thank-you',
|
|
468
|
-
'cs',
|
|
469
|
-
['en', 'cs'],
|
|
470
|
-
localisedUrls,
|
|
471
|
-
),
|
|
472
|
-
).toBe('/pokladna/dekujeme');
|
|
473
|
-
expect(
|
|
474
|
-
resolveLocalisedPath(
|
|
475
|
-
'/pokladna/dekujeme/ABC-123',
|
|
476
|
-
'en',
|
|
477
|
-
['en', 'cs'],
|
|
478
|
-
localisedUrls,
|
|
479
|
-
),
|
|
480
|
-
).toBe('/checkout/thank-you/ABC-123');
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
describe('matchPathPattern decoding', () => {
|
|
485
|
-
test('keeps prototype-named params as ordinary own values', () => {
|
|
486
|
-
const params = matchPathPattern('/products/red', '/products/:__proto__');
|
|
487
|
-
|
|
488
|
-
expect(params && Object.hasOwn(params, '__proto__')).toBe(true);
|
|
489
|
-
expect(
|
|
490
|
-
params && Object.getOwnPropertyDescriptor(params, '__proto__')?.value,
|
|
491
|
-
).toBe('red');
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test('decodes valid percent-encoded params', () => {
|
|
495
|
-
expect(
|
|
496
|
-
matchPathPattern('/produkty/%C4%8Derven%C3%A1', '/produkty/:slug'),
|
|
497
|
-
).toEqual({ slug: 'červená' });
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
test('treats malformed percent-encoding as no match instead of throwing', () => {
|
|
501
|
-
expect(() =>
|
|
502
|
-
matchPathPattern('/produkty/%E0%A4%A', '/produkty/:slug'),
|
|
503
|
-
).not.toThrow();
|
|
504
|
-
expect(
|
|
505
|
-
matchPathPattern('/produkty/%E0%A4%A', '/produkty/:slug'),
|
|
506
|
-
).toBeNull();
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test('keeps literal bracket segments in pathnames (no pattern rewrite)', () => {
|
|
510
|
-
expect(matchPathPattern('/produkty/[x]', '/produkty/:slug')).toEqual({
|
|
511
|
-
slug: '[x]',
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test('resolveLocalisedPath returns malformed paths unchanged', () => {
|
|
516
|
-
const localisedUrls = {
|
|
517
|
-
'/products/:slug': {
|
|
518
|
-
en: '/products/:slug',
|
|
519
|
-
cs: '/produkty/:slug',
|
|
520
|
-
},
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
expect(
|
|
524
|
-
resolveLocalisedPath(
|
|
525
|
-
'/produkty/%E0%A4%A',
|
|
526
|
-
'en',
|
|
527
|
-
['en', 'cs'],
|
|
528
|
-
localisedUrls,
|
|
529
|
-
),
|
|
530
|
-
).toBe('/produkty/%E0%A4%A');
|
|
531
|
-
});
|
|
532
|
-
});
|