@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.
Files changed (104) hide show
  1. package/dist/cjs/index.js +10 -10
  2. package/dist/cjs/localeRedirectPolicy.js +6 -6
  3. package/dist/cjs/native.js +160 -0
  4. package/dist/cjs/paths.js +11 -3
  5. package/dist/cjs/requestLanguage.js +195 -0
  6. package/dist/cjs/router-navigation/adapter.js +93 -0
  7. package/dist/cjs/{localisedUrls/config.js → router-navigation/index.js} +5 -14
  8. package/dist/cjs/ssrLanguageHandoff.js +94 -0
  9. package/dist/cjs/urlRewrite.js +64 -0
  10. package/dist/esm/index.mjs +1 -1
  11. package/dist/esm/localeRedirectPolicy.mjs +1 -1
  12. package/dist/esm/native.mjs +112 -0
  13. package/dist/esm/paths.mjs +7 -2
  14. package/dist/esm/requestLanguage.mjs +148 -0
  15. package/dist/esm/router-navigation/adapter.mjs +55 -0
  16. package/dist/esm/router-navigation/index.mjs +1 -0
  17. package/dist/esm/ssrLanguageHandoff.mjs +44 -0
  18. package/dist/esm/urlRewrite.mjs +26 -0
  19. package/dist/esm-node/index.mjs +1 -1
  20. package/dist/esm-node/localeRedirectPolicy.mjs +1 -1
  21. package/dist/esm-node/native.mjs +113 -0
  22. package/dist/esm-node/paths.mjs +7 -2
  23. package/dist/esm-node/requestLanguage.mjs +149 -0
  24. package/dist/esm-node/router-navigation/adapter.mjs +56 -0
  25. package/dist/esm-node/router-navigation/index.mjs +2 -0
  26. package/dist/esm-node/ssrLanguageHandoff.mjs +45 -0
  27. package/dist/esm-node/urlRewrite.mjs +27 -0
  28. package/dist/types/index.d.ts +1 -1
  29. package/dist/types/localeRedirectPolicy.d.ts +1 -1
  30. package/dist/types/native.d.ts +21 -0
  31. package/dist/types/paths.d.ts +8 -1
  32. package/dist/types/requestLanguage.d.ts +60 -0
  33. package/dist/types/router-navigation/adapter.d.ts +23 -0
  34. package/dist/types/router-navigation/index.d.ts +1 -0
  35. package/dist/types/ssrLanguageHandoff.d.ts +60 -0
  36. package/dist/types/urlRewrite.d.ts +60 -0
  37. package/package.json +101 -14
  38. package/src/index.ts +1 -1
  39. package/src/localeRedirectPolicy.ts +2 -2
  40. package/src/native.ts +212 -0
  41. package/src/paths.ts +23 -2
  42. package/src/requestLanguage.ts +311 -0
  43. package/src/router-navigation/adapter.tsx +120 -0
  44. package/src/router-navigation/index.ts +6 -0
  45. package/src/ssrLanguageHandoff.ts +138 -0
  46. package/src/urlRewrite.ts +107 -0
  47. package/dist/cjs/localisedUrls/index.js +0 -98
  48. package/dist/cjs/localisedUrls/normalise.js +0 -87
  49. package/dist/cjs/localisedUrls/pathname.js +0 -68
  50. package/dist/cjs/localisedUrls/patterns.js +0 -214
  51. package/dist/cjs/localisedUrls/redirect.js +0 -100
  52. package/dist/cjs/localisedUrls/resolve.js +0 -78
  53. package/dist/cjs/localisedUrls/routeIdentity.js +0 -118
  54. package/dist/cjs/localisedUrls/routes.js +0 -210
  55. package/dist/cjs/localisedUrls/types.js +0 -18
  56. package/dist/esm/localisedUrls/config.mjs +0 -11
  57. package/dist/esm/localisedUrls/index.mjs +0 -8
  58. package/dist/esm/localisedUrls/normalise.mjs +0 -37
  59. package/dist/esm/localisedUrls/pathname.mjs +0 -24
  60. package/dist/esm/localisedUrls/patterns.mjs +0 -152
  61. package/dist/esm/localisedUrls/redirect.mjs +0 -53
  62. package/dist/esm/localisedUrls/resolve.mjs +0 -37
  63. package/dist/esm/localisedUrls/routeIdentity.mjs +0 -77
  64. package/dist/esm/localisedUrls/routes.mjs +0 -169
  65. package/dist/esm/localisedUrls/types.mjs +0 -0
  66. package/dist/esm-node/localisedUrls/config.mjs +0 -12
  67. package/dist/esm-node/localisedUrls/index.mjs +0 -9
  68. package/dist/esm-node/localisedUrls/normalise.mjs +0 -38
  69. package/dist/esm-node/localisedUrls/pathname.mjs +0 -25
  70. package/dist/esm-node/localisedUrls/patterns.mjs +0 -153
  71. package/dist/esm-node/localisedUrls/redirect.mjs +0 -54
  72. package/dist/esm-node/localisedUrls/resolve.mjs +0 -38
  73. package/dist/esm-node/localisedUrls/routeIdentity.mjs +0 -78
  74. package/dist/esm-node/localisedUrls/routes.mjs +0 -170
  75. package/dist/esm-node/localisedUrls/types.mjs +0 -1
  76. package/dist/types/localisedUrls/config.d.ts +0 -9
  77. package/dist/types/localisedUrls/index.d.ts +0 -10
  78. package/dist/types/localisedUrls/normalise.d.ts +0 -10
  79. package/dist/types/localisedUrls/pathname.d.ts +0 -4
  80. package/dist/types/localisedUrls/patterns.d.ts +0 -11
  81. package/dist/types/localisedUrls/redirect.d.ts +0 -19
  82. package/dist/types/localisedUrls/resolve.d.ts +0 -7
  83. package/dist/types/localisedUrls/routeIdentity.d.ts +0 -24
  84. package/dist/types/localisedUrls/routes.d.ts +0 -3
  85. package/dist/types/localisedUrls/types.d.ts +0 -14
  86. package/rslib.config.mts +0 -4
  87. package/rstest.config.mts +0 -36
  88. package/src/localisedUrls/config.ts +0 -18
  89. package/src/localisedUrls/index.ts +0 -28
  90. package/src/localisedUrls/normalise.ts +0 -62
  91. package/src/localisedUrls/pathname.ts +0 -60
  92. package/src/localisedUrls/patterns.ts +0 -283
  93. package/src/localisedUrls/redirect.ts +0 -107
  94. package/src/localisedUrls/resolve.ts +0 -106
  95. package/src/localisedUrls/routeIdentity.ts +0 -107
  96. package/src/localisedUrls/routes.ts +0 -432
  97. package/src/localisedUrls/types.ts +0 -18
  98. package/tests/languageSyncController.test.ts +0 -136
  99. package/tests/localeRedirectPolicy.test.ts +0 -173
  100. package/tests/localisedUrls.test.ts +0 -532
  101. package/tests/paths.test.ts +0 -24
  102. package/tests/reactLanguageSync.test.tsx +0 -136
  103. package/tests/tsconfig.json +0 -11
  104. package/tsconfig.json +0 -11
@@ -1,283 +0,0 @@
1
- import { normalisePathname, normalisePathPattern } from './normalise';
2
-
3
- const escapeRegExp = (value: string): string =>
4
- value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
-
6
- const getParamName = (segment: string): string =>
7
- segment.slice(1).replace(/\?$/, '');
8
-
9
- export const getOrderedPathParamSignature = (pattern: string): string[] =>
10
- normalisePathPattern(pattern)
11
- .split('/')
12
- .filter(Boolean)
13
- .flatMap(segment => {
14
- if (segment === '*') {
15
- return ['*'];
16
- }
17
- if (!segment.startsWith(':')) {
18
- return [];
19
- }
20
-
21
- return [`:${getParamName(segment)}${segment.endsWith('?') ? '?' : ''}`];
22
- });
23
-
24
- export const getPathParamSignature = (pattern: string): string[] =>
25
- getOrderedPathParamSignature(pattern).sort((left, right) =>
26
- left < right ? -1 : left > right ? 1 : 0,
27
- );
28
-
29
- export const getPathPatternValidationIssue = (
30
- pattern: string,
31
- ): string | undefined => {
32
- const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
33
- const seenParams = new Set<string>();
34
-
35
- for (const [index, segment] of segments.entries()) {
36
- if (segment === '*') {
37
- if (index !== segments.length - 1) {
38
- return 'splat parameter "*" must be the final segment';
39
- }
40
- if (seenParams.has('*')) {
41
- return 'duplicate splat parameter "*"';
42
- }
43
- seenParams.add('*');
44
- continue;
45
- }
46
- if (!segment.startsWith(':')) {
47
- continue;
48
- }
49
-
50
- const paramName = getParamName(segment);
51
- if (seenParams.has(paramName)) {
52
- return `duplicate path parameter ":${paramName}"`;
53
- }
54
- seenParams.add(paramName);
55
- }
56
-
57
- return undefined;
58
- };
59
-
60
- export const getPhysicalPathPattern = (pattern: string): string => {
61
- const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
62
- const physicalSegments = segments.map(segment => {
63
- if (segment === '*') {
64
- return '*';
65
- }
66
- if (segment.startsWith(':')) {
67
- return segment.endsWith('?') ? ':param?' : ':param';
68
- }
69
- return segment;
70
- });
71
-
72
- return `/${physicalSegments.join('/')}`;
73
- };
74
-
75
- interface CompiledPathPattern {
76
- names: string[];
77
- regexp: RegExp;
78
- }
79
-
80
- const compiledPathPatternCache = new Map<string, CompiledPathPattern>();
81
-
82
- const compilePathPattern = (pattern: string): CompiledPathPattern => {
83
- const normalizedPattern = normalisePathPattern(pattern);
84
- const cached = compiledPathPatternCache.get(normalizedPattern);
85
- if (cached) {
86
- return cached;
87
- }
88
-
89
- const names: string[] = [];
90
- const segments = normalizedPattern.split('/').filter(Boolean);
91
- const source = segments
92
- .map(segment => {
93
- if (segment.startsWith(':')) {
94
- names.push(getParamName(segment));
95
- const paramPattern = '([^/]+)';
96
- return segment.endsWith('?')
97
- ? `(?:/${paramPattern})?`
98
- : `/${paramPattern}`;
99
- }
100
- if (segment === '*') {
101
- names.push('*');
102
- return '(?:/(.*))?';
103
- }
104
- return `/${escapeRegExp(segment)}`;
105
- })
106
- .join('');
107
-
108
- const compiled = {
109
- names,
110
- regexp: new RegExp(`^${source || '/'}$`),
111
- };
112
- compiledPathPatternCache.set(normalizedPattern, compiled);
113
-
114
- return compiled;
115
- };
116
-
117
- const getPatternSpecificity = (pattern: string) => {
118
- const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
119
- let staticSegments = 0;
120
- let dynamicSegments = 0;
121
- let optionalSegments = 0;
122
- let splatSegments = 0;
123
-
124
- for (const segment of segments) {
125
- if (segment === '*') {
126
- splatSegments++;
127
- } else if (segment.startsWith(':')) {
128
- dynamicSegments++;
129
- if (segment.endsWith('?')) {
130
- optionalSegments++;
131
- }
132
- } else {
133
- staticSegments++;
134
- }
135
- }
136
-
137
- return {
138
- staticSegments,
139
- dynamicSegments,
140
- optionalSegments,
141
- splatSegments,
142
- totalSegments: segments.length,
143
- };
144
- };
145
-
146
- const comparePatternSpecificity = (left: string, right: string): number => {
147
- const a = getPatternSpecificity(left);
148
- const b = getPatternSpecificity(right);
149
-
150
- return (
151
- b.staticSegments - a.staticSegments ||
152
- a.splatSegments - b.splatSegments ||
153
- a.optionalSegments - b.optionalSegments ||
154
- b.totalSegments - a.totalSegments ||
155
- a.dynamicSegments - b.dynamicSegments
156
- );
157
- };
158
-
159
- export const haveEqualPatternSpecificity = (
160
- left: string,
161
- right: string,
162
- ): boolean => comparePatternSpecificity(left, right) === 0;
163
-
164
- export const doPathPatternsOverlap = (left: string, right: string): boolean => {
165
- const leftSegments = normalisePathPattern(left).split('/').filter(Boolean);
166
- const rightSegments = normalisePathPattern(right).split('/').filter(Boolean);
167
- if (leftSegments.length !== rightSegments.length) {
168
- return false;
169
- }
170
-
171
- return leftSegments.every((leftSegment, index) => {
172
- const rightSegment = rightSegments[index];
173
- const leftIsDynamic = leftSegment === '*' || leftSegment.startsWith(':');
174
- const rightIsDynamic = rightSegment === '*' || rightSegment.startsWith(':');
175
- return leftIsDynamic || rightIsDynamic || leftSegment === rightSegment;
176
- });
177
- };
178
-
179
- const comparePatternNames = (left: string, right: string): number => {
180
- const normalizedLeft = normalisePathPattern(left);
181
- const normalizedRight = normalisePathPattern(right);
182
-
183
- return normalizedLeft < normalizedRight
184
- ? -1
185
- : normalizedLeft > normalizedRight
186
- ? 1
187
- : 0;
188
- };
189
-
190
- export const sortPatternsBySpecificity = <T extends { pattern: string }>(
191
- patterns: T[],
192
- ): T[] =>
193
- patterns
194
- .map((pattern, index) => ({ pattern, index }))
195
- .sort(
196
- (left, right) =>
197
- comparePatternSpecificity(
198
- left.pattern.pattern,
199
- right.pattern.pattern,
200
- ) ||
201
- comparePatternNames(left.pattern.pattern, right.pattern.pattern) ||
202
- left.index - right.index,
203
- )
204
- .map(({ pattern }) => pattern);
205
-
206
- /**
207
- * `decodeURIComponent` throws `URIError` on malformed percent-encoding
208
- * (e.g. `%E0%A4%A`), which attacker-controlled request URLs can carry.
209
- * Treat such segments as undecodable instead of throwing.
210
- */
211
- const decodePathParam = (value: string): string | null => {
212
- try {
213
- return decodeURIComponent(value);
214
- } catch {
215
- return null;
216
- }
217
- };
218
-
219
- const encodeSplatParam = (value: string): string =>
220
- value.split('/').map(encodeURIComponent).join('/');
221
-
222
- export const matchPathPattern = (
223
- pathname: string,
224
- pattern: string,
225
- ): Record<string, string> | null => {
226
- const { names, regexp } = compilePathPattern(pattern);
227
- const match = regexp.exec(normalisePathname(pathname));
228
- if (!match) {
229
- return null;
230
- }
231
-
232
- const params: Record<string, string> = {};
233
- for (let index = 0; index < names.length; index++) {
234
- const decoded = decodePathParam(match[index + 1] || '');
235
- if (decoded === null) {
236
- // Malformed encoding cannot identify a localised route: no match.
237
- return null;
238
- }
239
- Object.defineProperty(params, names[index], {
240
- configurable: true,
241
- enumerable: true,
242
- value: decoded,
243
- writable: true,
244
- });
245
- }
246
-
247
- return params;
248
- };
249
-
250
- export const buildPathFromPattern = (
251
- pattern: string,
252
- params: Record<string, string>,
253
- ): string => {
254
- const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
255
- const path = segments
256
- .map(segment => {
257
- if (segment.startsWith(':')) {
258
- const paramName = getParamName(segment);
259
- const param = Object.hasOwn(params, paramName)
260
- ? params[paramName]
261
- : undefined;
262
- if (param === undefined || param === '') {
263
- if (segment.endsWith('?')) {
264
- return '';
265
- }
266
- throw new Error(
267
- `Missing required path parameter "${paramName}" for pattern "${normalisePathPattern(
268
- pattern,
269
- )}".`,
270
- );
271
- }
272
- return encodeURIComponent(param);
273
- }
274
- if (segment === '*') {
275
- return encodeSplatParam(params['*'] || '');
276
- }
277
- return segment;
278
- })
279
- .filter(Boolean)
280
- .join('/');
281
-
282
- return `/${path}`;
283
- };
@@ -1,107 +0,0 @@
1
- import { normalisePathname } from './normalise';
2
- import { stripLanguagePrefix } from './pathname';
3
-
4
- export type LocaleRedirectSkipRule =
5
- | {
6
- type: 'exact';
7
- path: string;
8
- }
9
- | {
10
- type: 'prefix';
11
- path: string;
12
- }
13
- | {
14
- type: 'pattern';
15
- path: RegExp;
16
- };
17
-
18
- /**
19
- * Federation artifacts are never pages. The remote entries match the same
20
- * shapes the federation layer itself recognises and serves (`mfCache.ts`):
21
- * a manifest may name a root entry such as `remoteEntry.catalog.js`.
22
- */
23
- export const DEFAULT_LOCALE_REDIRECT_SKIP_RULES: readonly LocaleRedirectSkipRule[] =
24
- [
25
- { type: 'exact', path: '/backend-mf-manifest.json' },
26
- { type: 'exact', path: '/mf-manifest.json' },
27
- { type: 'exact', path: '/mf-stats.json' },
28
- {
29
- type: 'pattern',
30
- path: /^\/(?:backendRemoteEntry(?:\.[a-zA-Z0-9_-]+)?\.cjs|remoteEntry(?:\.[a-zA-Z0-9_-]+)?\.js)$/,
31
- },
32
- { type: 'prefix', path: '/static/' },
33
- { type: 'prefix', path: '/upload/' },
34
- ];
35
-
36
- const stripTrailingSlash = (pathname: string): string =>
37
- pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
38
-
39
- export const matchesPathPrefix = (
40
- pathname: string,
41
- prefix: string,
42
- ): boolean => {
43
- const normalizedPathname = normalisePathname(pathname);
44
- const normalizedPrefix = stripTrailingSlash(normalisePathname(prefix));
45
-
46
- return (
47
- normalizedPathname === normalizedPrefix ||
48
- normalizedPathname.startsWith(`${normalizedPrefix}/`)
49
- );
50
- };
51
-
52
- const matchesSkipRule = (
53
- pathname: string,
54
- rule: LocaleRedirectSkipRule,
55
- ): boolean => {
56
- if (rule.type === 'exact') {
57
- return normalisePathname(pathname) === normalisePathname(rule.path);
58
- }
59
- if (rule.type === 'pattern') {
60
- return rule.path.test(normalisePathname(pathname));
61
- }
62
-
63
- return matchesPathPrefix(pathname, rule.path);
64
- };
65
-
66
- export const isDefaultLocaleRedirectSkipPath = (
67
- pathname: string,
68
- languages: string[] = [],
69
- ): boolean => {
70
- const normalizedPathname = normalisePathname(pathname);
71
- const pathWithoutLanguage = stripLanguagePrefix(
72
- normalizedPathname,
73
- languages,
74
- );
75
-
76
- return DEFAULT_LOCALE_REDIRECT_SKIP_RULES.some(rule =>
77
- matchesSkipRule(pathWithoutLanguage, rule),
78
- );
79
- };
80
-
81
- export const shouldSkipLocaleRedirect = (
82
- pathname: string,
83
- languages: string[] = [],
84
- ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
85
- ): boolean => {
86
- const normalizedPathname = normalisePathname(pathname);
87
- const pathWithoutLanguage = stripLanguagePrefix(
88
- normalizedPathname,
89
- languages,
90
- );
91
-
92
- if (isDefaultLocaleRedirectSkipPath(pathWithoutLanguage)) {
93
- return true;
94
- }
95
-
96
- if (!ignoreRedirectRoutes) {
97
- return false;
98
- }
99
-
100
- if (typeof ignoreRedirectRoutes === 'function') {
101
- return ignoreRedirectRoutes(pathWithoutLanguage);
102
- }
103
-
104
- return ignoreRedirectRoutes.some(pattern =>
105
- matchesPathPrefix(pathWithoutLanguage, pattern),
106
- );
107
- };
@@ -1,106 +0,0 @@
1
- import { normalisePathname } from './normalise';
2
-
3
- import {
4
- buildPathFromPattern,
5
- matchPathPattern,
6
- sortPatternsBySpecificity,
7
- } from './patterns';
8
-
9
- import type { LocalisedUrlsMap } from './types';
10
-
11
- type CanonicalCandidate = {
12
- pattern: string;
13
- canonicalPattern: string;
14
- localisedUrlEntry: LocalisedUrlsMap[string];
15
- };
16
-
17
- type ResolveLocalisedUrlPathOptions =
18
- | {
19
- mode: 'localised';
20
- targetLanguage: string;
21
- }
22
- | {
23
- mode: 'canonical';
24
- };
25
-
26
- const getCanonicalCandidates = (
27
- localisedUrls: LocalisedUrlsMap,
28
- ): CanonicalCandidate[] =>
29
- sortPatternsBySpecificity(
30
- Object.entries(localisedUrls).map(
31
- ([canonicalPattern, localisedUrlEntry]) => ({
32
- pattern: canonicalPattern,
33
- canonicalPattern,
34
- localisedUrlEntry,
35
- }),
36
- ),
37
- );
38
-
39
- const resolveLocalisedUrlPath = (
40
- pathname: string,
41
- languages: string[],
42
- localisedUrls: LocalisedUrlsMap,
43
- options: ResolveLocalisedUrlPathOptions,
44
- ): string => {
45
- const normalizedPathname = normalisePathname(pathname);
46
- const canonicalCandidates = getCanonicalCandidates(localisedUrls);
47
-
48
- const candidates = sortPatternsBySpecificity(
49
- canonicalCandidates.flatMap(({ canonicalPattern, localisedUrlEntry }) => {
50
- const targetPattern =
51
- options.mode === 'localised'
52
- ? localisedUrlEntry[options.targetLanguage]
53
- : canonicalPattern;
54
- if (!targetPattern) {
55
- return [];
56
- }
57
-
58
- const sourcePatterns = new Set([
59
- canonicalPattern,
60
- ...languages
61
- .map(language => localisedUrlEntry[language])
62
- .filter((sourcePattern): sourcePattern is string =>
63
- Boolean(sourcePattern),
64
- ),
65
- ]);
66
- return Array.from(sourcePatterns, sourcePattern => ({
67
- pattern: sourcePattern,
68
- sourcePattern,
69
- targetPattern,
70
- }));
71
- }),
72
- );
73
-
74
- for (const { sourcePattern, targetPattern } of candidates) {
75
- const params = matchPathPattern(normalizedPathname, sourcePattern);
76
- if (params) {
77
- return buildPathFromPattern(targetPattern, params);
78
- }
79
- }
80
-
81
- return normalizedPathname;
82
- };
83
-
84
- export const resolveLocalisedPath = (
85
- pathname: string,
86
- targetLanguage: string,
87
- languages: string[],
88
- localisedUrls: LocalisedUrlsMap,
89
- ): string =>
90
- resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
91
- mode: 'localised',
92
- targetLanguage,
93
- });
94
-
95
- /**
96
- * Reverse-map language-specific pathnames back to language-agnostic
97
- * LocalisedUrlsMap keys.
98
- */
99
- export const resolveCanonicalLocalisedPath = (
100
- pathname: string,
101
- languages: string[],
102
- localisedUrls: LocalisedUrlsMap,
103
- ): string =>
104
- resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
105
- mode: 'canonical',
106
- });
@@ -1,107 +0,0 @@
1
- import { normalisePathPattern } from './normalise';
2
- import { buildPathFromPattern, matchPathPattern } from './patterns';
3
-
4
- interface RouteIdentity {
5
- id?: string;
6
- path?: string;
7
- canonicalPath: string;
8
- paths?: Record<string, string>;
9
- }
10
-
11
- interface RouteNode {
12
- id?: string;
13
- path?: string;
14
- children?: RouteNode[];
15
- modernLocalisedRoute?: RouteIdentity;
16
- }
17
-
18
- /** Restore one source route identity for routers with strict route-ID hooks. */
19
- export function canonicaliseLocalisedRoutes<T extends RouteNode>(
20
- routes: T[],
21
- ): T[] {
22
- const seen = new Set<string>();
23
- return routes.flatMap(route => {
24
- const identity = route.modernLocalisedRoute;
25
- const key = identity?.id ?? identity?.path;
26
- if (key !== undefined) {
27
- if (seen.has(key)) return [];
28
- seen.add(key);
29
- }
30
- return [
31
- {
32
- ...route,
33
- ...(identity ? { id: identity.id, path: identity.path } : {}),
34
- ...(route.children
35
- ? { children: canonicaliseLocalisedRoutes(route.children) }
36
- : {}),
37
- },
38
- ];
39
- });
40
- }
41
-
42
- type PathRewrite = { from: string; to: string };
43
-
44
- function rewritePath(url: URL, mappings: PathRewrite[]) {
45
- for (const { from, to } of mappings) {
46
- const params = matchPathPattern(url.pathname, from);
47
- if (params) {
48
- url.pathname = buildPathFromPattern(to, params);
49
- break;
50
- }
51
- }
52
- return url;
53
- }
54
-
55
- /** Use the router's URL rewrite seam; browser URLs retain their locale spelling. */
56
- export function createLocalisedRouteRewrite(routes: RouteNode[]) {
57
- const input: PathRewrite[] = [];
58
- const output: PathRewrite[] = [];
59
- const visit = (nodes: RouteNode[], parent: string) => {
60
- for (const route of nodes) {
61
- const fullPath = normalisePathPattern(`${parent}/${route.path ?? ''}`);
62
- const identity = route.modernLocalisedRoute;
63
- if (identity?.paths && fullPath.endsWith(identity.canonicalPath)) {
64
- const prefix = fullPath.slice(0, -identity.canonicalPath.length);
65
- const hasLocale = prefix
66
- .split('/')
67
- .some(segment => /^:(?:lang|locale|language)\??$/.test(segment));
68
- for (const [language, localisedPath] of Object.entries(
69
- identity.paths,
70
- )) {
71
- const localePrefix = prefix
72
- .split('/')
73
- .map(segment =>
74
- /^:(?:lang|locale|language)\??$/.test(segment)
75
- ? language
76
- : segment,
77
- )
78
- .join('/');
79
- const canonical = normalisePathPattern(
80
- `${localePrefix}/${identity.canonicalPath}`,
81
- );
82
- const localised = normalisePathPattern(
83
- `${localePrefix}/${localisedPath}`,
84
- );
85
- if (canonical !== localised) {
86
- input.push({ from: localised, to: canonical });
87
- if (hasLocale) output.push({ from: canonical, to: localised });
88
- }
89
- }
90
- }
91
- if (route.children) visit(route.children, fullPath);
92
- }
93
- };
94
- visit(canonicaliseLocalisedRoutes(routes), '');
95
- if (input.length === 0) return undefined;
96
- const specificity = (pattern: string) =>
97
- pattern
98
- .split('/')
99
- .filter(segment => segment && !segment.startsWith(':') && segment !== '*')
100
- .length;
101
- for (const mappings of [input, output])
102
- mappings.sort((a, b) => specificity(b.from) - specificity(a.from));
103
- return {
104
- input: ({ url }: { url: URL }) => rewritePath(url, input),
105
- output: ({ url }: { url: URL }) => rewritePath(url, output),
106
- };
107
- }