@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.4 → 3.5.0-ultramodern.40

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 (214) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/cli/locales.js +15 -15
  3. package/dist/cjs/runtime/Link.js +8 -89
  4. package/dist/cjs/runtime/context.js +18 -82
  5. package/dist/cjs/runtime/contextHelpers.js +137 -0
  6. package/dist/cjs/runtime/core.js +29 -132
  7. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  8. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  9. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  10. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  11. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  12. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  13. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  14. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  15. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  16. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  17. package/dist/cjs/runtime/i18n/instance.js +7 -7
  18. package/dist/cjs/runtime/linkHelpers.js +148 -0
  19. package/dist/cjs/runtime/pluginSetup.js +102 -0
  20. package/dist/cjs/runtime/providerComposition.js +114 -0
  21. package/dist/cjs/runtime/reactI18next.js +43 -0
  22. package/dist/cjs/runtime/utils.js +1 -9
  23. package/dist/cjs/server/apiPrefix.js +72 -0
  24. package/dist/cjs/server/detectorOptions.js +97 -0
  25. package/dist/cjs/server/index.js +17 -144
  26. package/dist/cjs/server/redirectPolicy.js +96 -0
  27. package/dist/cjs/shared/detection.js +2 -104
  28. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  29. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  30. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  31. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  32. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  33. package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
  34. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  35. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  36. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  37. package/dist/cjs/shared/localisedUrls.js +28 -294
  38. package/dist/cjs/shared/utils.js +2 -8
  39. package/dist/esm/cli/index.mjs +2 -1
  40. package/dist/esm/cli/locales.mjs +13 -13
  41. package/dist/esm/runtime/Link.mjs +1 -82
  42. package/dist/esm/runtime/context.mjs +18 -82
  43. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  44. package/dist/esm/runtime/core.mjs +30 -133
  45. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  46. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  47. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  48. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  49. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  50. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  51. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  52. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  54. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  55. package/dist/esm/runtime/i18n/instance.mjs +8 -5
  56. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  57. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  58. package/dist/esm/runtime/providerComposition.mjs +76 -0
  59. package/dist/esm/runtime/reactI18next.mjs +5 -0
  60. package/dist/esm/runtime/utils.mjs +2 -10
  61. package/dist/esm/server/apiPrefix.mjs +31 -0
  62. package/dist/esm/server/detectorOptions.mjs +59 -0
  63. package/dist/esm/server/index.mjs +8 -135
  64. package/dist/esm/server/redirectPolicy.mjs +46 -0
  65. package/dist/esm/shared/detection.mjs +1 -71
  66. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  67. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  68. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  69. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  70. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  71. package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
  72. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  73. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  74. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  75. package/dist/esm/shared/localisedUrls.mjs +1 -283
  76. package/dist/esm/shared/utils.mjs +1 -1
  77. package/dist/esm-node/cli/index.mjs +2 -1
  78. package/dist/esm-node/cli/locales.mjs +13 -13
  79. package/dist/esm-node/runtime/Link.mjs +1 -82
  80. package/dist/esm-node/runtime/context.mjs +18 -82
  81. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  82. package/dist/esm-node/runtime/core.mjs +30 -133
  83. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  84. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  85. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  86. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  87. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  88. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  89. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  90. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  91. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  92. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  93. package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
  94. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  95. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  96. package/dist/esm-node/runtime/providerComposition.mjs +77 -0
  97. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  98. package/dist/esm-node/runtime/utils.mjs +2 -10
  99. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  100. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  101. package/dist/esm-node/server/index.mjs +8 -135
  102. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  103. package/dist/esm-node/shared/detection.mjs +1 -71
  104. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  105. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  106. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  107. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  108. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  109. package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
  110. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  111. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  112. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  113. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  114. package/dist/esm-node/shared/utils.mjs +1 -1
  115. package/dist/types/cli/locales.d.ts +1 -1
  116. package/dist/types/runtime/Link.d.ts +3 -7
  117. package/dist/types/runtime/context.d.ts +6 -5
  118. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  119. package/dist/types/runtime/core.d.ts +3 -7
  120. package/dist/types/runtime/hooks.d.ts +5 -0
  121. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  122. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  123. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  124. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  125. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  126. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  127. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  128. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  129. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  130. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  131. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  132. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  133. package/dist/types/runtime/i18n/instance.d.ts +28 -26
  134. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  135. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  136. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  137. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  138. package/dist/types/runtime/providerComposition.d.ts +16 -0
  139. package/dist/types/runtime/reactI18next.d.ts +7 -0
  140. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  141. package/dist/types/runtime/utils.d.ts +7 -2
  142. package/dist/types/server/apiPrefix.d.ts +7 -0
  143. package/dist/types/server/detectorOptions.d.ts +21 -0
  144. package/dist/types/server/index.d.ts +1 -6
  145. package/dist/types/server/redirectPolicy.d.ts +22 -0
  146. package/dist/types/shared/detection.d.ts +1 -11
  147. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  148. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  149. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  150. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  151. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  152. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  153. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  154. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  155. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  156. package/dist/types/shared/localisedUrls.d.ts +2 -36
  157. package/dist/types/shared/utils.d.ts +0 -2
  158. package/package.json +16 -18
  159. package/rstest.config.mts +1 -0
  160. package/src/cli/index.ts +5 -4
  161. package/src/cli/locales.ts +3 -3
  162. package/src/runtime/Link.tsx +11 -138
  163. package/src/runtime/context.tsx +45 -199
  164. package/src/runtime/contextHelpers.ts +232 -0
  165. package/src/runtime/core.tsx +43 -274
  166. package/src/runtime/hooks.ts +13 -2
  167. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  168. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  169. package/src/runtime/i18n/detection/cache.ts +189 -0
  170. package/src/runtime/i18n/detection/detector.ts +166 -0
  171. package/src/runtime/i18n/detection/index.ts +10 -641
  172. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  173. package/src/runtime/i18n/detection/language.ts +64 -0
  174. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  175. package/src/runtime/i18n/detection/middleware.ts +15 -7
  176. package/src/runtime/i18n/detection/path.ts +41 -0
  177. package/src/runtime/i18n/detection/priority.ts +82 -0
  178. package/src/runtime/i18n/detection/ssr.ts +47 -0
  179. package/src/runtime/i18n/detection/types.ts +41 -0
  180. package/src/runtime/i18n/instance.ts +56 -31
  181. package/src/runtime/i18n/react-i18next.ts +1 -6
  182. package/src/runtime/i18n/utils.ts +18 -9
  183. package/src/runtime/linkHelpers.ts +174 -0
  184. package/src/runtime/pluginSetup.ts +189 -0
  185. package/src/runtime/providerComposition.tsx +144 -0
  186. package/src/runtime/reactI18next.ts +20 -0
  187. package/src/runtime/routerAdapter.tsx +6 -6
  188. package/src/runtime/utils.ts +13 -35
  189. package/src/server/apiPrefix.ts +67 -0
  190. package/src/server/detectorOptions.ts +105 -0
  191. package/src/server/index.ts +14 -319
  192. package/src/server/redirectPolicy.ts +127 -0
  193. package/src/shared/detection.ts +1 -1
  194. package/src/shared/localisedUrls/config.ts +18 -0
  195. package/src/shared/localisedUrls/index.ts +24 -0
  196. package/src/shared/localisedUrls/normalise.ts +67 -0
  197. package/src/shared/localisedUrls/pathname.ts +60 -0
  198. package/src/shared/localisedUrls/patterns.ts +160 -0
  199. package/src/shared/localisedUrls/redirect.ts +91 -0
  200. package/src/shared/localisedUrls/resolve.ts +135 -0
  201. package/src/shared/localisedUrls/routes.ts +226 -0
  202. package/src/shared/localisedUrls/types.ts +18 -0
  203. package/src/shared/localisedUrls.ts +26 -623
  204. package/src/shared/utils.ts +2 -2
  205. package/tests/fixtures/localised-routes.golden.json +156 -0
  206. package/tests/link.test.tsx +111 -0
  207. package/tests/linkTypes.test.ts +15 -4
  208. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  209. package/tests/localisedUrls.test.ts +129 -2
  210. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  211. package/tests/redirectPolicy.test.ts +43 -0
  212. package/tests/routerAdapter.test.tsx +85 -0
  213. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  214. package/tests/type-fixture/tsconfig.json +1 -0
@@ -0,0 +1,67 @@
1
+ const LOCALE_PARAM_NAMES = new Set(['lang', 'locale', 'language']);
2
+
3
+ const normaliseSlashes = (path: string): string => {
4
+ const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
5
+ const withLeadingSlash = withoutDuplicateSlashes.startsWith('/')
6
+ ? withoutDuplicateSlashes
7
+ : `/${withoutDuplicateSlashes}`;
8
+
9
+ return withLeadingSlash.length > 1
10
+ ? withLeadingSlash.replace(/\/+$/, '')
11
+ : withLeadingSlash;
12
+ };
13
+
14
+ export const normalisePathPattern = (path: string): string =>
15
+ normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
16
+
17
+ /**
18
+ * Normalise a concrete request pathname: slash cleanup only. Unlike
19
+ * {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
20
+ * `:x` params — pathnames are values, not patterns.
21
+ */
22
+ export const normalisePathname = (pathname: string): string =>
23
+ normaliseSlashes(pathname);
24
+
25
+ export const normaliseRoutePath = (path: string): string => {
26
+ const normalized = normalisePathPattern(path);
27
+ return normalized === '/' ? '' : normalized.slice(1);
28
+ };
29
+
30
+ const getLocaleParamSegment = (segment: string): string | null => {
31
+ if (!segment.startsWith(':')) {
32
+ return null;
33
+ }
34
+
35
+ const paramName = segment.slice(1).replace(/\?$/, '');
36
+ return LOCALE_PARAM_NAMES.has(paramName) ? segment : null;
37
+ };
38
+
39
+ const splitPathSegments = (path?: string): string[] => {
40
+ if (!path) {
41
+ return [];
42
+ }
43
+
44
+ return normalisePathPattern(path).split('/').filter(Boolean);
45
+ };
46
+
47
+ export const stripLeadingLocaleParam = (path?: string): string | undefined => {
48
+ const segments = splitPathSegments(path);
49
+ const leadingLocaleParam = getLocaleParamSegment(segments[0] || '');
50
+
51
+ if (!leadingLocaleParam) {
52
+ return path;
53
+ }
54
+
55
+ const remainingPath = segments.slice(1).join('/');
56
+ return remainingPath ? `/${remainingPath}` : undefined;
57
+ };
58
+
59
+ export const getLeadingLocaleParam = (path?: string): string | null => {
60
+ const segments = splitPathSegments(path);
61
+ return getLocaleParamSegment(segments[0] || '');
62
+ };
63
+
64
+ const isLocaleParamPath = (path?: string): boolean => {
65
+ const segments = splitPathSegments(path);
66
+ return segments.length === 1 && Boolean(getLocaleParamSegment(segments[0]));
67
+ };
@@ -0,0 +1,60 @@
1
+ import { resolveLocalisedUrlsConfig } from './config';
2
+
3
+ import { resolveCanonicalLocalisedPath, resolveLocalisedPath } from './resolve';
4
+
5
+ import type { LocalisedUrlsOption } from './types';
6
+
7
+ export const stripLanguagePrefix = (
8
+ pathname: string,
9
+ languages: string[],
10
+ ): string => {
11
+ const segments = pathname.split('/').filter(Boolean);
12
+ const firstSegment = segments[0]?.toLowerCase();
13
+
14
+ if (
15
+ firstSegment &&
16
+ languages.some(language => language.toLowerCase() === firstSegment)
17
+ ) {
18
+ return `/${segments.slice(1).join('/')}`;
19
+ }
20
+
21
+ return pathname || '/';
22
+ };
23
+
24
+ export const localiseTargetPathname = (
25
+ pathname: string,
26
+ language: string,
27
+ languages: string[],
28
+ localisedUrls?: LocalisedUrlsOption,
29
+ ): string => {
30
+ const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
31
+ const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
32
+ const resolvedPath = localisedUrlsConfig.enabled
33
+ ? resolveLocalisedPath(
34
+ pathWithoutLanguage,
35
+ language,
36
+ languages,
37
+ localisedUrlsConfig.map,
38
+ )
39
+ : pathWithoutLanguage;
40
+ const resolvedSegments = resolvedPath.split('/').filter(Boolean);
41
+
42
+ return `/${[language, ...resolvedSegments].join('/')}`;
43
+ };
44
+
45
+ export const canonicalTargetPathname = (
46
+ pathname: string,
47
+ languages: string[],
48
+ localisedUrls?: LocalisedUrlsOption,
49
+ ): string => {
50
+ const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
51
+ const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
52
+
53
+ return localisedUrlsConfig.enabled
54
+ ? resolveCanonicalLocalisedPath(
55
+ pathWithoutLanguage,
56
+ languages,
57
+ localisedUrlsConfig.map,
58
+ )
59
+ : pathWithoutLanguage;
60
+ };
@@ -0,0 +1,160 @@
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
+ interface CompiledPathPattern {
10
+ names: string[];
11
+ regexp: RegExp;
12
+ }
13
+
14
+ const compiledPathPatternCache = new Map<string, CompiledPathPattern>();
15
+
16
+ const compilePathPattern = (pattern: string): CompiledPathPattern => {
17
+ const normalizedPattern = normalisePathPattern(pattern);
18
+ const cached = compiledPathPatternCache.get(normalizedPattern);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+
23
+ const names: string[] = [];
24
+ const segments = normalizedPattern.split('/').filter(Boolean);
25
+ const source = segments
26
+ .map(segment => {
27
+ if (segment.startsWith(':')) {
28
+ names.push(getParamName(segment));
29
+ const paramPattern = '([^/]+)';
30
+ return segment.endsWith('?')
31
+ ? `(?:/${paramPattern})?`
32
+ : `/${paramPattern}`;
33
+ }
34
+ if (segment === '*') {
35
+ names.push('*');
36
+ return '/(.*)';
37
+ }
38
+ return `/${escapeRegExp(segment)}`;
39
+ })
40
+ .join('');
41
+
42
+ const compiled = {
43
+ names,
44
+ regexp: new RegExp(`^${source || '/'}$`),
45
+ };
46
+ compiledPathPatternCache.set(normalizedPattern, compiled);
47
+
48
+ return compiled;
49
+ };
50
+
51
+ const getPatternSpecificity = (pattern: string) => {
52
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
53
+ let staticSegments = 0;
54
+ let dynamicSegments = 0;
55
+ let splatSegments = 0;
56
+
57
+ for (const segment of segments) {
58
+ if (segment === '*') {
59
+ splatSegments++;
60
+ } else if (segment.startsWith(':')) {
61
+ dynamicSegments++;
62
+ } else {
63
+ staticSegments++;
64
+ }
65
+ }
66
+
67
+ return {
68
+ staticSegments,
69
+ dynamicSegments,
70
+ splatSegments,
71
+ totalSegments: segments.length,
72
+ };
73
+ };
74
+
75
+ const comparePatternSpecificity = (left: string, right: string): number => {
76
+ const a = getPatternSpecificity(left);
77
+ const b = getPatternSpecificity(right);
78
+
79
+ return (
80
+ b.staticSegments - a.staticSegments ||
81
+ b.totalSegments - a.totalSegments ||
82
+ a.splatSegments - b.splatSegments ||
83
+ a.dynamicSegments - b.dynamicSegments
84
+ );
85
+ };
86
+
87
+ export const sortPatternsBySpecificity = <T extends { pattern: string }>(
88
+ patterns: T[],
89
+ ): T[] =>
90
+ patterns
91
+ .map((pattern, index) => ({ pattern, index }))
92
+ .sort(
93
+ (left, right) =>
94
+ comparePatternSpecificity(
95
+ left.pattern.pattern,
96
+ right.pattern.pattern,
97
+ ) || left.index - right.index,
98
+ )
99
+ .map(({ pattern }) => pattern);
100
+
101
+ /**
102
+ * `decodeURIComponent` throws `URIError` on malformed percent-encoding
103
+ * (e.g. `%E0%A4%A`), which attacker-controlled request URLs can carry.
104
+ * Treat such segments as undecodable instead of throwing.
105
+ */
106
+ const decodePathParam = (value: string): string | null => {
107
+ try {
108
+ return decodeURIComponent(value);
109
+ } catch {
110
+ return null;
111
+ }
112
+ };
113
+
114
+ const encodeSplatParam = (value: string): string =>
115
+ value.split('/').map(encodeURIComponent).join('/');
116
+
117
+ export const matchPathPattern = (
118
+ pathname: string,
119
+ pattern: string,
120
+ ): Record<string, string> | null => {
121
+ const { names, regexp } = compilePathPattern(pattern);
122
+ const match = regexp.exec(normalisePathname(pathname));
123
+ if (!match) {
124
+ return null;
125
+ }
126
+
127
+ const params: Record<string, string> = {};
128
+ for (let index = 0; index < names.length; index++) {
129
+ const decoded = decodePathParam(match[index + 1] || '');
130
+ if (decoded === null) {
131
+ // Malformed encoding cannot identify a localised route: no match.
132
+ return null;
133
+ }
134
+ params[names[index]] = decoded;
135
+ }
136
+
137
+ return params;
138
+ };
139
+
140
+ export const buildPathFromPattern = (
141
+ pattern: string,
142
+ params: Record<string, string>,
143
+ ): string => {
144
+ const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
145
+ const path = segments
146
+ .map(segment => {
147
+ if (segment.startsWith(':')) {
148
+ const param = params[getParamName(segment)];
149
+ return param ? encodeURIComponent(param) : '';
150
+ }
151
+ if (segment === '*') {
152
+ return encodeSplatParam(params['*'] || '');
153
+ }
154
+ return segment;
155
+ })
156
+ .filter(Boolean)
157
+ .join('/');
158
+
159
+ return `/${path}`;
160
+ };
@@ -0,0 +1,91 @@
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
+ export const DEFAULT_LOCALE_REDIRECT_SKIP_RULES: readonly LocaleRedirectSkipRule[] =
15
+ [
16
+ { type: 'exact', path: '/mf-manifest.json' },
17
+ { type: 'exact', path: '/mf-stats.json' },
18
+ { type: 'exact', path: '/remoteEntry.js' },
19
+ { type: 'prefix', path: '/static/' },
20
+ { type: 'prefix', path: '/upload/' },
21
+ ];
22
+
23
+ const stripTrailingSlash = (pathname: string): string =>
24
+ pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
25
+
26
+ export const matchesPathPrefix = (
27
+ pathname: string,
28
+ prefix: string,
29
+ ): boolean => {
30
+ const normalizedPathname = normalisePathname(pathname);
31
+ const normalizedPrefix = stripTrailingSlash(normalisePathname(prefix));
32
+
33
+ return (
34
+ normalizedPathname === normalizedPrefix ||
35
+ normalizedPathname.startsWith(`${normalizedPrefix}/`)
36
+ );
37
+ };
38
+
39
+ const matchesSkipRule = (
40
+ pathname: string,
41
+ rule: LocaleRedirectSkipRule,
42
+ ): boolean => {
43
+ if (rule.type === 'exact') {
44
+ return normalisePathname(pathname) === normalisePathname(rule.path);
45
+ }
46
+
47
+ return matchesPathPrefix(pathname, rule.path);
48
+ };
49
+
50
+ export const isDefaultLocaleRedirectSkipPath = (
51
+ pathname: string,
52
+ languages: string[] = [],
53
+ ): boolean => {
54
+ const normalizedPathname = normalisePathname(pathname);
55
+ const pathWithoutLanguage = stripLanguagePrefix(
56
+ normalizedPathname,
57
+ languages,
58
+ );
59
+
60
+ return DEFAULT_LOCALE_REDIRECT_SKIP_RULES.some(rule =>
61
+ matchesSkipRule(pathWithoutLanguage, rule),
62
+ );
63
+ };
64
+
65
+ export const shouldSkipLocaleRedirect = (
66
+ pathname: string,
67
+ languages: string[] = [],
68
+ ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
69
+ ): boolean => {
70
+ const normalizedPathname = normalisePathname(pathname);
71
+ const pathWithoutLanguage = stripLanguagePrefix(
72
+ normalizedPathname,
73
+ languages,
74
+ );
75
+
76
+ if (isDefaultLocaleRedirectSkipPath(pathWithoutLanguage)) {
77
+ return true;
78
+ }
79
+
80
+ if (!ignoreRedirectRoutes) {
81
+ return false;
82
+ }
83
+
84
+ if (typeof ignoreRedirectRoutes === 'function') {
85
+ return ignoreRedirectRoutes(pathWithoutLanguage);
86
+ }
87
+
88
+ return ignoreRedirectRoutes.some(pattern =>
89
+ matchesPathPrefix(pathWithoutLanguage, pattern),
90
+ );
91
+ };
@@ -0,0 +1,135 @@
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
+ for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates) {
49
+ const targetPattern =
50
+ options.mode === 'localised'
51
+ ? localisedUrlEntry[options.targetLanguage]
52
+ : canonicalPattern;
53
+
54
+ if (!targetPattern) {
55
+ continue;
56
+ }
57
+
58
+ const canonicalParams = matchPathPattern(
59
+ normalizedPathname,
60
+ canonicalPattern,
61
+ );
62
+ if (canonicalParams) {
63
+ return buildPathFromPattern(targetPattern, canonicalParams);
64
+ }
65
+
66
+ if (options.mode === 'canonical') {
67
+ for (const language of languages) {
68
+ const sourcePattern = localisedUrlEntry[language];
69
+ if (!sourcePattern) {
70
+ continue;
71
+ }
72
+
73
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
74
+ if (params) {
75
+ return buildPathFromPattern(canonicalPattern, params);
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ if (options.mode === 'localised') {
82
+ const localisedCandidates = sortPatternsBySpecificity(
83
+ Object.values(localisedUrls).flatMap(localisedUrlEntry => {
84
+ const targetPattern = localisedUrlEntry[options.targetLanguage];
85
+ if (!targetPattern) {
86
+ return [];
87
+ }
88
+
89
+ return languages
90
+ .map(language => localisedUrlEntry[language])
91
+ .filter((sourcePattern): sourcePattern is string =>
92
+ Boolean(sourcePattern),
93
+ )
94
+ .map(sourcePattern => ({
95
+ pattern: sourcePattern,
96
+ sourcePattern,
97
+ targetPattern,
98
+ }));
99
+ }),
100
+ );
101
+
102
+ for (const { sourcePattern, targetPattern } of localisedCandidates) {
103
+ const params = matchPathPattern(normalizedPathname, sourcePattern);
104
+ if (params) {
105
+ return buildPathFromPattern(targetPattern, params);
106
+ }
107
+ }
108
+ }
109
+
110
+ return normalizedPathname;
111
+ };
112
+
113
+ export const resolveLocalisedPath = (
114
+ pathname: string,
115
+ targetLanguage: string,
116
+ languages: string[],
117
+ localisedUrls: LocalisedUrlsMap,
118
+ ): string =>
119
+ resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
120
+ mode: 'localised',
121
+ targetLanguage,
122
+ });
123
+
124
+ /**
125
+ * Reverse-map language-specific pathnames back to language-agnostic
126
+ * LocalisedUrlsMap keys.
127
+ */
128
+ export const resolveCanonicalLocalisedPath = (
129
+ pathname: string,
130
+ languages: string[],
131
+ localisedUrls: LocalisedUrlsMap,
132
+ ): string =>
133
+ resolveLocalisedUrlPath(pathname, languages, localisedUrls, {
134
+ mode: 'canonical',
135
+ });