@mpgd/i18n 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/paraglide/messages/_index.d.ts +59 -58
- package/dist/paraglide/messages/_index.js +6 -6
- package/dist/paraglide/messages/action_interstitial_ads.d.ts +4 -4
- package/dist/paraglide/messages/action_interstitial_ads.js +2 -2
- package/dist/paraglide/messages/action_leaderboard.d.ts +4 -4
- package/dist/paraglide/messages/action_leaderboard.js +2 -2
- package/dist/paraglide/messages/action_localization.d.ts +4 -4
- package/dist/paraglide/messages/action_localization.js +2 -2
- package/dist/paraglide/messages/action_purchases.d.ts +4 -4
- package/dist/paraglide/messages/action_purchases.js +2 -2
- package/dist/paraglide/messages/action_reward_ads.d.ts +4 -4
- package/dist/paraglide/messages/action_reward_ads.js +2 -2
- package/dist/paraglide/messages/app_title.d.ts +4 -4
- package/dist/paraglide/messages/app_title.js +2 -2
- package/dist/paraglide/messages/availability_off.d.ts +4 -4
- package/dist/paraglide/messages/availability_off.js +2 -2
- package/dist/paraglide/messages/availability_on.d.ts +4 -4
- package/dist/paraglide/messages/availability_on.js +2 -2
- package/dist/paraglide/messages/availability_unsupported.d.ts +4 -4
- package/dist/paraglide/messages/availability_unsupported.js +2 -2
- package/dist/paraglide/messages/cap_iap.d.ts +4 -4
- package/dist/paraglide/messages/cap_iap.js +2 -2
- package/dist/paraglide/messages/cap_leaderboard.d.ts +4 -4
- package/dist/paraglide/messages/cap_leaderboard.js +2 -2
- package/dist/paraglide/messages/cap_localization.d.ts +4 -4
- package/dist/paraglide/messages/cap_localization.js +2 -2
- package/dist/paraglide/messages/cap_rewarded_ads.d.ts +4 -4
- package/dist/paraglide/messages/cap_rewarded_ads.js +2 -2
- package/dist/paraglide/messages/cap_save.d.ts +4 -4
- package/dist/paraglide/messages/cap_save.js +2 -2
- package/dist/paraglide/messages/effective_config_summary.d.ts +8 -8
- package/dist/paraglide/messages/effective_config_summary.js +2 -2
- package/dist/paraglide/messages/feature_target_disabled.d.ts +6 -6
- package/dist/paraglide/messages/feature_target_disabled.js +2 -2
- package/dist/paraglide/messages/feature_unavailable.d.ts +6 -6
- package/dist/paraglide/messages/feature_unavailable.js +2 -2
- package/dist/paraglide/messages/feature_unsupported.d.ts +6 -6
- package/dist/paraglide/messages/feature_unsupported.js +2 -2
- package/dist/paraglide/messages/leaderboard_action.d.ts +4 -4
- package/dist/paraglide/messages/leaderboard_action.js +2 -2
- package/dist/paraglide/messages/leaderboard_action_unavailable.d.ts +4 -4
- package/dist/paraglide/messages/leaderboard_action_unavailable.js +2 -2
- package/dist/paraglide/messages/leaderboard_opened.d.ts +4 -4
- package/dist/paraglide/messages/leaderboard_opened.js +2 -2
- package/dist/paraglide/messages/leaderboard_unavailable.d.ts +4 -4
- package/dist/paraglide/messages/leaderboard_unavailable.js +2 -2
- package/dist/paraglide/messages/loading_player.d.ts +4 -4
- package/dist/paraglide/messages/loading_player.js +2 -2
- package/dist/paraglide/messages/mock_only.d.ts +4 -4
- package/dist/paraglide/messages/mock_only.js +2 -2
- package/dist/paraglide/messages/opening_purchase.d.ts +4 -4
- package/dist/paraglide/messages/opening_purchase.js +2 -2
- package/dist/paraglide/messages/play_again.d.ts +4 -4
- package/dist/paraglide/messages/play_again.js +2 -2
- package/dist/paraglide/messages/player.d.ts +6 -6
- package/dist/paraglide/messages/player.js +2 -2
- package/dist/paraglide/messages/preparing_demo.d.ts +4 -4
- package/dist/paraglide/messages/preparing_demo.js +2 -2
- package/dist/paraglide/messages/purchase_action.d.ts +4 -4
- package/dist/paraglide/messages/purchase_action.js +2 -2
- package/dist/paraglide/messages/purchase_completed.d.ts +4 -4
- package/dist/paraglide/messages/purchase_completed.js +2 -2
- package/dist/paraglide/messages/purchase_status.d.ts +6 -6
- package/dist/paraglide/messages/purchase_status.js +2 -2
- package/dist/paraglide/messages/purchase_unavailable.d.ts +4 -4
- package/dist/paraglide/messages/purchase_unavailable.js +2 -2
- package/dist/paraglide/messages/reward_ad_action.d.ts +4 -4
- package/dist/paraglide/messages/reward_ad_action.js +2 -2
- package/dist/paraglide/messages/reward_ad_unavailable.d.ts +4 -4
- package/dist/paraglide/messages/reward_ad_unavailable.js +2 -2
- package/dist/paraglide/messages/reward_granted.d.ts +4 -4
- package/dist/paraglide/messages/reward_granted.js +2 -2
- package/dist/paraglide/messages/reward_unavailable.d.ts +6 -6
- package/dist/paraglide/messages/reward_unavailable.js +2 -2
- package/dist/paraglide/messages/save_summary.d.ts +7 -7
- package/dist/paraglide/messages/save_summary.js +2 -2
- package/dist/paraglide/messages/saved.d.ts +4 -4
- package/dist/paraglide/messages/saved.js +2 -2
- package/dist/paraglide/messages/saved_and_submitted.d.ts +4 -4
- package/dist/paraglide/messages/saved_and_submitted.js +2 -2
- package/dist/paraglide/messages/saving_result.d.ts +4 -4
- package/dist/paraglide/messages/saving_result.js +2 -2
- package/dist/paraglide/messages/score.d.ts +6 -6
- package/dist/paraglide/messages/score.js +2 -2
- package/dist/paraglide/messages/sdk_summary.d.ts +6 -6
- package/dist/paraglide/messages/sdk_summary.js +2 -2
- package/dist/paraglide/messages/showing_rewarded_ad.d.ts +4 -4
- package/dist/paraglide/messages/showing_rewarded_ad.js +2 -2
- package/dist/paraglide/messages/status_cleared.d.ts +4 -4
- package/dist/paraglide/messages/status_cleared.js +2 -2
- package/dist/paraglide/messages/status_try_again.d.ts +4 -4
- package/dist/paraglide/messages/status_try_again.js +2 -2
- package/dist/paraglide/messages/tap_to_start.d.ts +4 -4
- package/dist/paraglide/messages/tap_to_start.js +2 -2
- package/dist/paraglide/messages/target.d.ts +6 -6
- package/dist/paraglide/messages/target.js +2 -2
- package/dist/paraglide/messages/target_availability_summary.d.ts +7 -7
- package/dist/paraglide/messages/target_availability_summary.js +2 -2
- package/dist/paraglide/messages/target_config_unavailable.d.ts +4 -4
- package/dist/paraglide/messages/target_config_unavailable.js +2 -2
- package/dist/paraglide/messages/target_feature_iap.d.ts +4 -4
- package/dist/paraglide/messages/target_feature_iap.js +2 -2
- package/dist/paraglide/messages/target_feature_interstitial_ads.d.ts +4 -4
- package/dist/paraglide/messages/target_feature_interstitial_ads.js +2 -2
- package/dist/paraglide/messages/target_feature_leaderboard.d.ts +4 -4
- package/dist/paraglide/messages/target_feature_leaderboard.js +2 -2
- package/dist/paraglide/messages/target_feature_localization.d.ts +4 -4
- package/dist/paraglide/messages/target_feature_localization.js +2 -2
- package/dist/paraglide/messages/target_feature_rewarded_ads.d.ts +4 -4
- package/dist/paraglide/messages/target_feature_rewarded_ads.js +2 -2
- package/dist/paraglide/messages/viewport.d.ts +8 -8
- package/dist/paraglide/messages/viewport.js +2 -2
- package/dist/paraglide/messages/viewport_orientation_mismatch.d.ts +7 -7
- package/dist/paraglide/messages/viewport_orientation_mismatch.js +2 -2
- package/dist/paraglide/messages/viewport_orientation_policy.d.ts +6 -6
- package/dist/paraglide/messages/viewport_orientation_policy.js +2 -2
- package/dist/paraglide/messages.d.ts +2 -2
- package/dist/paraglide/registry.d.ts +5 -5
- package/dist/paraglide/runtime.d.ts +548 -283
- package/dist/paraglide/runtime.js +545 -86
- package/dist/paraglide/server.d.ts +1 -1
- package/dist/paraglide/server.js +20 -8
- package/package.json +3 -3
|
@@ -69,62 +69,12 @@ export const urlPatterns = [
|
|
|
69
69
|
]
|
|
70
70
|
}
|
|
71
71
|
];
|
|
72
|
-
/** @type {string | undefined} */
|
|
73
|
-
let cachedRouteStrategyUrl;
|
|
74
|
-
/** @type {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined} */
|
|
75
|
-
let cachedRouteStrategy;
|
|
76
|
-
/**
|
|
77
|
-
* @param {string | URL} url
|
|
78
|
-
* @returns {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined}
|
|
79
|
-
*/
|
|
80
|
-
function findMatchingRouteStrategy(url) {
|
|
81
|
-
if (routeStrategies.length === 0) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
const urlString = typeof url === "string" ? url : url.href;
|
|
85
|
-
if (cachedRouteStrategyUrl === urlString) {
|
|
86
|
-
return cachedRouteStrategy;
|
|
87
|
-
}
|
|
88
|
-
const urlObject = new URL(urlString, "http://dummy.com");
|
|
89
|
-
let match;
|
|
90
|
-
for (const routeStrategy of routeStrategies) {
|
|
91
|
-
const pattern = new URLPattern(routeStrategy.match, urlObject.href);
|
|
92
|
-
if (pattern.exec(urlObject.href)) {
|
|
93
|
-
match = routeStrategy;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
cachedRouteStrategyUrl = urlString;
|
|
98
|
-
cachedRouteStrategy = match;
|
|
99
|
-
return match;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Returns the strategy to use for a specific URL.
|
|
103
|
-
*
|
|
104
|
-
* If no route strategy matches (or the matching rule is `exclude: true`),
|
|
105
|
-
* the global strategy is returned.
|
|
106
|
-
*
|
|
107
|
-
* @param {string | URL} url
|
|
108
|
-
* @returns {typeof strategy}
|
|
109
|
-
*/
|
|
110
|
-
export function getStrategyForUrl(url) {
|
|
111
|
-
const routeStrategy = findMatchingRouteStrategy(url);
|
|
112
|
-
if (routeStrategy &&
|
|
113
|
-
routeStrategy.exclude !== true &&
|
|
114
|
-
Array.isArray(routeStrategy.strategy)) {
|
|
115
|
-
return routeStrategy.strategy;
|
|
116
|
-
}
|
|
117
|
-
return strategy;
|
|
118
|
-
}
|
|
119
72
|
/**
|
|
120
|
-
*
|
|
73
|
+
* Controls trailing slash canonicalization for localized URLs.
|
|
121
74
|
*
|
|
122
|
-
* @
|
|
123
|
-
* @returns {boolean}
|
|
75
|
+
* @type {"always" | "never" | undefined}
|
|
124
76
|
*/
|
|
125
|
-
export
|
|
126
|
-
return findMatchingRouteStrategy(url)?.exclude === true;
|
|
127
|
-
}
|
|
77
|
+
export const trailingSlash = undefined;
|
|
128
78
|
/**
|
|
129
79
|
* @typedef {{
|
|
130
80
|
* getStore(): {
|
|
@@ -145,6 +95,17 @@ export function isExcludedByRouteStrategy(url) {
|
|
|
145
95
|
* @type {ParaglideAsyncLocalStorage | undefined}
|
|
146
96
|
*/
|
|
147
97
|
export let serverAsyncLocalStorage = undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Returns the current server-side async local storage instance.
|
|
100
|
+
*
|
|
101
|
+
* Accessing the mutable value through a function keeps it observable when
|
|
102
|
+
* module interceptors wrap exported bindings and snapshot their initial value.
|
|
103
|
+
*
|
|
104
|
+
* @returns {ParaglideAsyncLocalStorage | undefined}
|
|
105
|
+
*/
|
|
106
|
+
export function getServerAsyncLocalStorage() {
|
|
107
|
+
return serverAsyncLocalStorage;
|
|
108
|
+
}
|
|
148
109
|
export const disableAsyncLocalStorage = false;
|
|
149
110
|
export const experimentalMiddlewareLocaleSplitting = false;
|
|
150
111
|
export const isServer = typeof window === 'undefined';
|
|
@@ -392,9 +353,12 @@ const navigateOrReload = (newLocation) => {
|
|
|
392
353
|
* Set the locale.
|
|
393
354
|
*
|
|
394
355
|
* Updates the locale using your configured strategies (cookie, localStorage, URL, etc.).
|
|
395
|
-
* By default, this
|
|
396
|
-
*
|
|
397
|
-
*
|
|
356
|
+
* By default, this navigates the client to the localized URL or reloads the current
|
|
357
|
+
* document to reflect the new locale. `reload: false` is a narrow browser-only escape
|
|
358
|
+
* hatch for a fully client-rendered, non-URL-routed surface that owns its reactive
|
|
359
|
+
* updates and document state. It does not re-render the UI or update the document.
|
|
360
|
+
* Do not use it for normal locale pickers, URL-routed pages, or switching an SSR,
|
|
361
|
+
* SSG, or hydrated document. It is incompatible with per-locale builds.
|
|
398
362
|
*
|
|
399
363
|
* If any custom strategy's `setLocale` function is async, then this function
|
|
400
364
|
* will become async as well.
|
|
@@ -414,6 +378,12 @@ export let setLocale = (newLocale, options) => {
|
|
|
414
378
|
reload: true,
|
|
415
379
|
...options,
|
|
416
380
|
};
|
|
381
|
+
if (experimentalStaticLocale !== undefined &&
|
|
382
|
+
newLocale !== experimentalStaticLocale &&
|
|
383
|
+
optionsWithDefaults.reload === false) {
|
|
384
|
+
console.warn(`Paraglide: setLocale(${JSON.stringify(newLocale)}, { reload: false }) cannot switch away from the statically built locale ${JSON.stringify(experimentalStaticLocale)}. A document navigation is required; reload has been forced to true.`);
|
|
385
|
+
optionsWithDefaults.reload = true;
|
|
386
|
+
}
|
|
417
387
|
// locale is already set
|
|
418
388
|
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
419
389
|
/** @type {Locale | undefined} */
|
|
@@ -530,7 +500,7 @@ export const overwriteSetLocale = (fn) => {
|
|
|
530
500
|
/**
|
|
531
501
|
* The origin of the current URL.
|
|
532
502
|
*
|
|
533
|
-
* Defaults to "http://
|
|
503
|
+
* Defaults to "http://example.com" in non-browser environments. If this
|
|
534
504
|
* behavior is not desired, the implementation can be overwritten
|
|
535
505
|
* by `overwriteGetUrlOrigin()`.
|
|
536
506
|
*
|
|
@@ -607,6 +577,58 @@ export function assertIsLocale(input) {
|
|
|
607
577
|
throw new Error(`Invalid locale: ${input}. Expected one of: ${locales.join(", ")}`);
|
|
608
578
|
}
|
|
609
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Applies the configured trailing slash policy to a URL.
|
|
582
|
+
*
|
|
583
|
+
* The root pathname always remains `/`. Query parameters and hashes are not
|
|
584
|
+
* modified.
|
|
585
|
+
*
|
|
586
|
+
* @param {URL} url
|
|
587
|
+
* @returns {URL}
|
|
588
|
+
*/
|
|
589
|
+
function normalizeTrailingSlash(url) {
|
|
590
|
+
if (trailingSlash === undefined || url.pathname === "/") {
|
|
591
|
+
return url;
|
|
592
|
+
}
|
|
593
|
+
if (trailingSlash === "never") {
|
|
594
|
+
url.pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
595
|
+
}
|
|
596
|
+
else if (trailingSlash === "always") {
|
|
597
|
+
url.pathname = url.pathname.replace(/\/+$/, "") + "/";
|
|
598
|
+
}
|
|
599
|
+
return url;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Matches a canonical URL while allowing configured patterns to retain their
|
|
604
|
+
* existing trailing slash style.
|
|
605
|
+
*
|
|
606
|
+
* @param {URLPattern} pattern
|
|
607
|
+
* @param {URL} url
|
|
608
|
+
* @returns {any}
|
|
609
|
+
*/
|
|
610
|
+
function execUrlPattern(pattern, url) {
|
|
611
|
+
if (trailingSlash === undefined || url.pathname === "/") {
|
|
612
|
+
return pattern.exec(url.href);
|
|
613
|
+
}
|
|
614
|
+
const alias = new URL(url);
|
|
615
|
+
if (trailingSlash === "always") {
|
|
616
|
+
alias.pathname = alias.pathname.replace(/\/+$/, "") || "/";
|
|
617
|
+
// Prefer the slashless alias so the canonical slash does not become part
|
|
618
|
+
// of a terminal wildcard capture.
|
|
619
|
+
return pattern.exec(alias.href) ?? pattern.exec(url.href);
|
|
620
|
+
}
|
|
621
|
+
else if (trailingSlash === "never") {
|
|
622
|
+
const match = pattern.exec(url.href);
|
|
623
|
+
if (match) {
|
|
624
|
+
return match;
|
|
625
|
+
}
|
|
626
|
+
alias.pathname = alias.pathname.replace(/\/+$/, "") + "/";
|
|
627
|
+
return pattern.exec(alias.href);
|
|
628
|
+
}
|
|
629
|
+
return pattern.exec(url.href);
|
|
630
|
+
}
|
|
631
|
+
|
|
610
632
|
/**
|
|
611
633
|
* @typedef {object} ExtractLocaleFromRequestOptions
|
|
612
634
|
* @property {string | URL} [effectiveRequestUrl] - Effective request URL to use for route matching and locale detection with the URL strategy.
|
|
@@ -908,14 +930,17 @@ export function extractLocaleFromUrl(url) {
|
|
|
908
930
|
/** @type {Locale | undefined} */
|
|
909
931
|
let result;
|
|
910
932
|
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
911
|
-
|
|
933
|
+
const urlObj = typeof url === "string"
|
|
934
|
+
? new URL(url, "http://example.com")
|
|
935
|
+
: new URL(url);
|
|
936
|
+
result = defaultUrlPatternExtractLocale(normalizeTrailingSlash(urlObj));
|
|
912
937
|
}
|
|
913
938
|
else {
|
|
914
|
-
const urlObj = typeof url === "string" ? new URL(url) : url;
|
|
939
|
+
const urlObj = normalizeTrailingSlash(typeof url === "string" ? new URL(url) : new URL(url));
|
|
915
940
|
// Iterate over URL patterns
|
|
916
941
|
for (const element of urlPatterns) {
|
|
917
942
|
for (const [locale, localizedPattern] of element.localized) {
|
|
918
|
-
const match = new URLPattern(localizedPattern, urlObj.href)
|
|
943
|
+
const match = execUrlPattern(new URLPattern(localizedPattern, urlObj.href), urlObj);
|
|
919
944
|
if (match) {
|
|
920
945
|
result = locale;
|
|
921
946
|
break;
|
|
@@ -936,7 +961,7 @@ export function extractLocaleFromUrl(url) {
|
|
|
936
961
|
* @returns {Locale | undefined} The extracted locale, or undefined if no locale is found.
|
|
937
962
|
*/
|
|
938
963
|
function defaultUrlPatternExtractLocale(url) {
|
|
939
|
-
const urlObj = new URL(url, "http://
|
|
964
|
+
const urlObj = new URL(url, "http://example.com");
|
|
940
965
|
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
941
966
|
return toLocale(pathSegments[0]) || baseLocale;
|
|
942
967
|
}
|
|
@@ -992,12 +1017,24 @@ export function localizeUrl(url, options) {
|
|
|
992
1017
|
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
993
1018
|
return localizeUrlDefaultPattern(url, targetLocale);
|
|
994
1019
|
}
|
|
995
|
-
const
|
|
1020
|
+
const originalUrl = typeof url === "string" ? new URL(url) : url;
|
|
1021
|
+
const urlObj = trailingSlash === undefined
|
|
1022
|
+
? originalUrl
|
|
1023
|
+
: normalizeTrailingSlash(new URL(originalUrl));
|
|
996
1024
|
// Iterate over URL patterns
|
|
997
1025
|
for (const element of urlPatterns) {
|
|
1026
|
+
// Most applications use a locale prefix (and sometimes a fixed domain)
|
|
1027
|
+
// with a trailing catch-all path. Those routes do not need URLPattern's
|
|
1028
|
+
// parser or matcher on every call. Keep the generic matcher below as the
|
|
1029
|
+
// fallback for every other URLPattern shape. This stays inside the outer
|
|
1030
|
+
// loop so specific patterns retain precedence over a later catch-all.
|
|
1031
|
+
const fastPathLocalized = localizeUrlFastPath(urlObj, targetLocale, element);
|
|
1032
|
+
if (fastPathLocalized !== undefined) {
|
|
1033
|
+
return fastPathLocalized;
|
|
1034
|
+
}
|
|
998
1035
|
// match localized patterns
|
|
999
1036
|
for (const [, localizedPattern] of element.localized) {
|
|
1000
|
-
const match =
|
|
1037
|
+
const match = execUrlPattern(getUrlPattern(localizedPattern, urlObj), urlObj);
|
|
1001
1038
|
if (!match) {
|
|
1002
1039
|
continue;
|
|
1003
1040
|
}
|
|
@@ -1006,19 +1043,19 @@ export function localizeUrl(url, options) {
|
|
|
1006
1043
|
continue;
|
|
1007
1044
|
}
|
|
1008
1045
|
const localizedUrl = fillPattern(targetPattern, aggregateGroups(match), urlObj.origin);
|
|
1009
|
-
return fillMissingUrlParts(localizedUrl, match);
|
|
1046
|
+
return normalizeTrailingSlash(fillMissingUrlParts(localizedUrl, match));
|
|
1010
1047
|
}
|
|
1011
|
-
const unlocalizedMatch =
|
|
1048
|
+
const unlocalizedMatch = execUrlPattern(getUrlPattern(element.pattern, urlObj), urlObj);
|
|
1012
1049
|
if (unlocalizedMatch) {
|
|
1013
1050
|
const targetPattern = element.localized.find(([locale]) => locale === targetLocale)?.[1];
|
|
1014
1051
|
if (targetPattern) {
|
|
1015
1052
|
const localizedUrl = fillPattern(targetPattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
|
|
1016
|
-
return fillMissingUrlParts(localizedUrl, unlocalizedMatch);
|
|
1053
|
+
return normalizeTrailingSlash(fillMissingUrlParts(localizedUrl, unlocalizedMatch));
|
|
1017
1054
|
}
|
|
1018
1055
|
}
|
|
1019
1056
|
}
|
|
1020
1057
|
// If no match found, return the original URL
|
|
1021
|
-
return
|
|
1058
|
+
return originalUrl;
|
|
1022
1059
|
}
|
|
1023
1060
|
/**
|
|
1024
1061
|
* https://github.com/opral/inlang-paraglide-js/issues/381
|
|
@@ -1028,11 +1065,11 @@ export function localizeUrl(url, options) {
|
|
|
1028
1065
|
* @returns {URL}
|
|
1029
1066
|
*/
|
|
1030
1067
|
function localizeUrlDefaultPattern(url, locale) {
|
|
1031
|
-
const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
|
|
1068
|
+
const urlObj = normalizeTrailingSlash(typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url));
|
|
1032
1069
|
const currentLocale = extractLocaleFromUrl(urlObj);
|
|
1033
1070
|
// If current locale matches target locale, no change needed
|
|
1034
1071
|
if (currentLocale === locale) {
|
|
1035
|
-
return urlObj;
|
|
1072
|
+
return normalizeTrailingSlash(urlObj);
|
|
1036
1073
|
}
|
|
1037
1074
|
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
1038
1075
|
// If current path starts with a locale, remove it
|
|
@@ -1047,7 +1084,7 @@ function localizeUrlDefaultPattern(url, locale) {
|
|
|
1047
1084
|
// For other locales, add prefix
|
|
1048
1085
|
urlObj.pathname = "/" + locale + "/" + pathSegments.join("/");
|
|
1049
1086
|
}
|
|
1050
|
-
return urlObj;
|
|
1087
|
+
return normalizeTrailingSlash(urlObj);
|
|
1051
1088
|
}
|
|
1052
1089
|
/**
|
|
1053
1090
|
* Low-level URL de-localization function, primarily used in server contexts.
|
|
@@ -1092,28 +1129,35 @@ export function deLocalizeUrl(url) {
|
|
|
1092
1129
|
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
1093
1130
|
return deLocalizeUrlDefaultPattern(url);
|
|
1094
1131
|
}
|
|
1095
|
-
const
|
|
1132
|
+
const originalUrl = typeof url === "string" ? new URL(url) : url;
|
|
1133
|
+
const urlObj = trailingSlash === undefined
|
|
1134
|
+
? originalUrl
|
|
1135
|
+
: normalizeTrailingSlash(new URL(originalUrl));
|
|
1096
1136
|
// Iterate over URL patterns
|
|
1097
1137
|
for (const element of urlPatterns) {
|
|
1138
|
+
const fastPathDeLocalized = deLocalizeUrlFastPath(urlObj, element);
|
|
1139
|
+
if (fastPathDeLocalized !== undefined) {
|
|
1140
|
+
return fastPathDeLocalized;
|
|
1141
|
+
}
|
|
1098
1142
|
// Iterate over localized versions
|
|
1099
1143
|
for (const [, localizedPattern] of element.localized) {
|
|
1100
|
-
const match =
|
|
1144
|
+
const match = execUrlPattern(getUrlPattern(localizedPattern, urlObj), urlObj);
|
|
1101
1145
|
if (match) {
|
|
1102
1146
|
// Convert localized URL back to the base pattern
|
|
1103
1147
|
const groups = aggregateGroups(match);
|
|
1104
1148
|
const baseUrl = fillPattern(element.pattern, groups, urlObj.origin);
|
|
1105
|
-
return fillMissingUrlParts(baseUrl, match);
|
|
1149
|
+
return normalizeTrailingSlash(fillMissingUrlParts(baseUrl, match));
|
|
1106
1150
|
}
|
|
1107
1151
|
}
|
|
1108
1152
|
// match unlocalized pattern
|
|
1109
|
-
const unlocalizedMatch =
|
|
1153
|
+
const unlocalizedMatch = execUrlPattern(getUrlPattern(element.pattern, urlObj), urlObj);
|
|
1110
1154
|
if (unlocalizedMatch) {
|
|
1111
1155
|
const baseUrl = fillPattern(element.pattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
|
|
1112
|
-
return fillMissingUrlParts(baseUrl, unlocalizedMatch);
|
|
1156
|
+
return normalizeTrailingSlash(fillMissingUrlParts(baseUrl, unlocalizedMatch));
|
|
1113
1157
|
}
|
|
1114
1158
|
}
|
|
1115
1159
|
// no match found return the original url
|
|
1116
|
-
return
|
|
1160
|
+
return originalUrl;
|
|
1117
1161
|
}
|
|
1118
1162
|
/**
|
|
1119
1163
|
* De-localizes a URL using the default pattern (/:locale/*)
|
|
@@ -1121,13 +1165,13 @@ export function deLocalizeUrl(url) {
|
|
|
1121
1165
|
* @returns {URL}
|
|
1122
1166
|
*/
|
|
1123
1167
|
function deLocalizeUrlDefaultPattern(url) {
|
|
1124
|
-
const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
|
|
1168
|
+
const urlObj = normalizeTrailingSlash(typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url));
|
|
1125
1169
|
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
1126
1170
|
// If first segment is a locale, remove it
|
|
1127
1171
|
if (pathSegments.length > 0 && toLocale(pathSegments[0])) {
|
|
1128
1172
|
urlObj.pathname = "/" + pathSegments.slice(1).join("/");
|
|
1129
1173
|
}
|
|
1130
|
-
return urlObj;
|
|
1174
|
+
return normalizeTrailingSlash(urlObj);
|
|
1131
1175
|
}
|
|
1132
1176
|
/**
|
|
1133
1177
|
* Takes matches of implicit wildcards in the UrlPattern (when a part is missing
|
|
@@ -1252,6 +1296,413 @@ export function aggregateGroups(match) {
|
|
|
1252
1296
|
...match.username.groups,
|
|
1253
1297
|
};
|
|
1254
1298
|
}
|
|
1299
|
+
/** @type {Map<string, URLPattern>} */
|
|
1300
|
+
const urlPatternCache = new Map();
|
|
1301
|
+
const URL_PATTERN_CACHE_LIMIT = 128;
|
|
1302
|
+
const ABSOLUTE_URL_PATTERN = /^(?:[A-Za-z][A-Za-z\d+.-]*|:[A-Za-z][A-Za-z\d_-]*):\/\//;
|
|
1303
|
+
/**
|
|
1304
|
+
* URLPattern's base URL affects relative patterns. Absolute patterns only
|
|
1305
|
+
* depend on the pattern itself, while root-relative patterns also depend on
|
|
1306
|
+
* the URL origin. Other relative patterns are deliberately not cached because
|
|
1307
|
+
* their semantics depend on the complete base URL.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {string} pattern
|
|
1310
|
+
* @param {URL} url
|
|
1311
|
+
* @returns {URLPattern}
|
|
1312
|
+
*/
|
|
1313
|
+
function getUrlPattern(pattern, url) {
|
|
1314
|
+
const isAbsolutePattern = ABSOLUTE_URL_PATTERN.test(pattern);
|
|
1315
|
+
const isRootRelativePattern = pattern.startsWith("/");
|
|
1316
|
+
if (!isAbsolutePattern && !isRootRelativePattern) {
|
|
1317
|
+
return new URLPattern(pattern, url.href);
|
|
1318
|
+
}
|
|
1319
|
+
const key = isAbsolutePattern
|
|
1320
|
+
? pattern
|
|
1321
|
+
: JSON.stringify([url.origin, pattern]);
|
|
1322
|
+
const cached = urlPatternCache.get(key);
|
|
1323
|
+
if (cached !== undefined) {
|
|
1324
|
+
// Refresh the entry so frequently used patterns stay in the bounded cache.
|
|
1325
|
+
urlPatternCache.delete(key);
|
|
1326
|
+
urlPatternCache.set(key, cached);
|
|
1327
|
+
return cached;
|
|
1328
|
+
}
|
|
1329
|
+
const compiled = new URLPattern(pattern, url.href);
|
|
1330
|
+
if (urlPatternCache.size >= URL_PATTERN_CACHE_LIMIT) {
|
|
1331
|
+
const oldestKey = urlPatternCache.keys().next().value;
|
|
1332
|
+
if (oldestKey !== undefined)
|
|
1333
|
+
urlPatternCache.delete(oldestKey);
|
|
1334
|
+
}
|
|
1335
|
+
urlPatternCache.set(key, compiled);
|
|
1336
|
+
return compiled;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* A small, deliberately conservative subset of URLPattern routing.
|
|
1340
|
+
*
|
|
1341
|
+
* The compiler emits routes such as `/:path(.*)?`, `/de/:path*`, or
|
|
1342
|
+
* `https://example.com/:path*`. For those routes matching is equivalent to a
|
|
1343
|
+
* pathname prefix check and (optionally) an origin check. Everything that has
|
|
1344
|
+
* a dynamic host, a custom path regexp, or another URLPattern modifier keeps
|
|
1345
|
+
* using the generic implementation above.
|
|
1346
|
+
*
|
|
1347
|
+
* @typedef {{
|
|
1348
|
+
* protocol: string | undefined;
|
|
1349
|
+
* hostname: string | undefined;
|
|
1350
|
+
* port: string | undefined;
|
|
1351
|
+
* pathnamePrefix: string;
|
|
1352
|
+
* pathMode: "segments" | "catch-all-optional" | "catch-all-required";
|
|
1353
|
+
* }} FastPathPattern
|
|
1354
|
+
* @typedef {{
|
|
1355
|
+
* base: FastPathPattern;
|
|
1356
|
+
* localized: Array<{ locale: string; pattern: FastPathPattern }>;
|
|
1357
|
+
* }} FastPathRoute
|
|
1358
|
+
*/
|
|
1359
|
+
/** @type {WeakMap<object, FastPathRoute | null>} */
|
|
1360
|
+
const fastPathRouteCache = new WeakMap();
|
|
1361
|
+
/**
|
|
1362
|
+
* @param {URL} urlObj
|
|
1363
|
+
* @param {string} targetLocale
|
|
1364
|
+
* @param {{ pattern: string; localized: Array<[string, string]> }} element
|
|
1365
|
+
* @returns {URL | undefined}
|
|
1366
|
+
*/
|
|
1367
|
+
function localizeUrlFastPath(urlObj, targetLocale, element) {
|
|
1368
|
+
const route = getFastPathRoute(element);
|
|
1369
|
+
if (route === null)
|
|
1370
|
+
return undefined;
|
|
1371
|
+
// Preserve URLPattern's ordering: localized patterns are checked before
|
|
1372
|
+
// the unlocalized pattern, and the first matching pattern wins.
|
|
1373
|
+
for (const localized of route.localized) {
|
|
1374
|
+
const suffix = matchFastPathPattern(localized.pattern, urlObj);
|
|
1375
|
+
if (suffix === undefined)
|
|
1376
|
+
continue;
|
|
1377
|
+
const target = route.localized.find((candidate) => candidate.locale === targetLocale)?.pattern;
|
|
1378
|
+
if (target === undefined)
|
|
1379
|
+
continue;
|
|
1380
|
+
return applyFastPathPattern(target, suffix, urlObj);
|
|
1381
|
+
}
|
|
1382
|
+
const suffix = matchFastPathPattern(route.base, urlObj);
|
|
1383
|
+
if (suffix !== undefined) {
|
|
1384
|
+
const target = route.localized.find((candidate) => candidate.locale === targetLocale)?.pattern;
|
|
1385
|
+
if (target !== undefined) {
|
|
1386
|
+
return applyFastPathPattern(target, suffix, urlObj);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return undefined;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* @param {URL} urlObj
|
|
1393
|
+
* @param {{ pattern: string; localized: Array<[string, string]> }} element
|
|
1394
|
+
* @returns {URL | undefined}
|
|
1395
|
+
*/
|
|
1396
|
+
function deLocalizeUrlFastPath(urlObj, element) {
|
|
1397
|
+
const route = getFastPathRoute(element);
|
|
1398
|
+
if (route === null)
|
|
1399
|
+
return undefined;
|
|
1400
|
+
for (const localized of route.localized) {
|
|
1401
|
+
const suffix = matchFastPathPattern(localized.pattern, urlObj);
|
|
1402
|
+
if (suffix !== undefined) {
|
|
1403
|
+
return applyFastPathPattern(route.base, suffix, urlObj);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const suffix = matchFastPathPattern(route.base, urlObj);
|
|
1407
|
+
if (suffix !== undefined) {
|
|
1408
|
+
return applyFastPathPattern(route.base, suffix, urlObj);
|
|
1409
|
+
}
|
|
1410
|
+
return undefined;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* @param {{ pattern: string; localized: Array<[string, string]> }} element
|
|
1414
|
+
* @returns {FastPathRoute | null}
|
|
1415
|
+
*/
|
|
1416
|
+
function getFastPathRoute(element) {
|
|
1417
|
+
const cached = fastPathRouteCache.get(element);
|
|
1418
|
+
if (cached !== undefined)
|
|
1419
|
+
return cached;
|
|
1420
|
+
const base = parseFastPathPattern(element.pattern);
|
|
1421
|
+
if (base === undefined) {
|
|
1422
|
+
fastPathRouteCache.set(element, null);
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
const localized = [];
|
|
1426
|
+
for (const [locale, pattern] of element.localized) {
|
|
1427
|
+
const parsed = parseFastPathPattern(pattern);
|
|
1428
|
+
if (parsed === undefined || parsed.pathMode !== base.pathMode) {
|
|
1429
|
+
fastPathRouteCache.set(element, null);
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
localized.push({ locale, pattern: parsed });
|
|
1433
|
+
}
|
|
1434
|
+
const route = { base, localized };
|
|
1435
|
+
fastPathRouteCache.set(element, route);
|
|
1436
|
+
return route;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Parse only catch-all path patterns. In particular, do not treat `:path(.)?`
|
|
1440
|
+
* as a catch-all: URLPattern's `(.)` means exactly one character and cannot be
|
|
1441
|
+
* represented by this prefix matcher without changing routing semantics.
|
|
1442
|
+
*
|
|
1443
|
+
* @param {string} pattern
|
|
1444
|
+
* @returns {FastPathPattern | undefined}
|
|
1445
|
+
*/
|
|
1446
|
+
function parseFastPathPattern(pattern) {
|
|
1447
|
+
const wildcard = pattern.match(/\/:path(?:\(\.\*\)(?:\?)?|\*)$/);
|
|
1448
|
+
if (wildcard === null || wildcard.index === undefined)
|
|
1449
|
+
return undefined;
|
|
1450
|
+
const prefix = pattern.slice(0, wildcard.index);
|
|
1451
|
+
const originAndPath = parseFastPathOriginAndPath(prefix);
|
|
1452
|
+
if (originAndPath === undefined)
|
|
1453
|
+
return undefined;
|
|
1454
|
+
const pathMode = wildcard[0].endsWith("*")
|
|
1455
|
+
? "segments"
|
|
1456
|
+
: wildcard[0].endsWith("?")
|
|
1457
|
+
? "catch-all-optional"
|
|
1458
|
+
: "catch-all-required";
|
|
1459
|
+
return { ...originAndPath, pathMode };
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* @param {string} prefix
|
|
1463
|
+
* @returns {FastPathPattern | undefined}
|
|
1464
|
+
*/
|
|
1465
|
+
function parseFastPathOriginAndPath(prefix) {
|
|
1466
|
+
if (prefix === "" || prefix.startsWith("/")) {
|
|
1467
|
+
if (hasUrlPatternSyntax(prefix))
|
|
1468
|
+
return undefined;
|
|
1469
|
+
return {
|
|
1470
|
+
protocol: undefined,
|
|
1471
|
+
hostname: undefined,
|
|
1472
|
+
port: undefined,
|
|
1473
|
+
pathnamePrefix: normalizePathPrefix(prefix),
|
|
1474
|
+
pathMode: "catch-all-optional",
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
// A `:protocol://host` pattern is common in generated configurations. The
|
|
1478
|
+
// protocol is intentionally left unconstrained, just like URLPattern.
|
|
1479
|
+
const dynamicProtocol = prefix.match(/^:protocol:\/\/([^/]+)(\/.*)?$/);
|
|
1480
|
+
const staticOrigin = prefix.match(/^([A-Za-z][A-Za-z\d+.-]*):\/\/([^/]+)(\/.*)?$/);
|
|
1481
|
+
if (dynamicProtocol !== null) {
|
|
1482
|
+
const dynamicHost = dynamicProtocol[1] ?? "";
|
|
1483
|
+
const hostMatch = dynamicHost.match(/^([^:(){}?*+]+)(?::(\d+))?$/);
|
|
1484
|
+
if (hostMatch === null ||
|
|
1485
|
+
(dynamicProtocol[2] !== undefined &&
|
|
1486
|
+
hasUrlPatternSyntax(dynamicProtocol[2]))) {
|
|
1487
|
+
return undefined;
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
protocol: undefined,
|
|
1491
|
+
hostname: (hostMatch[1] ?? "").toLowerCase(),
|
|
1492
|
+
port: hostMatch[2],
|
|
1493
|
+
pathnamePrefix: normalizePathPrefix(dynamicProtocol[2] ?? ""),
|
|
1494
|
+
pathMode: "catch-all-optional",
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
if (staticOrigin === null)
|
|
1498
|
+
return undefined;
|
|
1499
|
+
const host = staticOrigin[2] ?? "";
|
|
1500
|
+
const pathname = staticOrigin[3];
|
|
1501
|
+
const hostMatch = host.match(/^([^:(){}?*+]+)(?::(\d+))?$/);
|
|
1502
|
+
if (hostMatch === null ||
|
|
1503
|
+
(pathname !== undefined && hasUrlPatternSyntax(pathname))) {
|
|
1504
|
+
return undefined;
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
protocol: `${staticOrigin[1]}:`,
|
|
1508
|
+
hostname: (hostMatch[1] ?? "").toLowerCase(),
|
|
1509
|
+
port: normalizePatternPort(hostMatch[2], `${staticOrigin[1]}:`),
|
|
1510
|
+
pathnamePrefix: normalizePathPrefix(pathname ?? ""),
|
|
1511
|
+
pathMode: "catch-all-optional",
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* @param {string | undefined} port
|
|
1516
|
+
* @param {string} protocol
|
|
1517
|
+
* @returns {string | undefined}
|
|
1518
|
+
*/
|
|
1519
|
+
function normalizePatternPort(port, protocol) {
|
|
1520
|
+
if (port === "80" && protocol === "http:")
|
|
1521
|
+
return "";
|
|
1522
|
+
if (port === "443" && protocol === "https:")
|
|
1523
|
+
return "";
|
|
1524
|
+
return port;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* @param {string} value
|
|
1528
|
+
* @returns {boolean}
|
|
1529
|
+
*/
|
|
1530
|
+
function hasUrlPatternSyntax(value) {
|
|
1531
|
+
return /[:(){}?*+]/.test(value);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* @param {string} pathname
|
|
1535
|
+
* @returns {string}
|
|
1536
|
+
*/
|
|
1537
|
+
function normalizePathPrefix(pathname) {
|
|
1538
|
+
if (pathname === "" || pathname === "/")
|
|
1539
|
+
return "/";
|
|
1540
|
+
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* @param {FastPathPattern} pattern
|
|
1544
|
+
* @param {URL} urlObj
|
|
1545
|
+
* @returns {string | undefined}
|
|
1546
|
+
*/
|
|
1547
|
+
function matchFastPathPattern(pattern, urlObj) {
|
|
1548
|
+
if (pattern.protocol !== undefined && pattern.protocol !== urlObj.protocol) {
|
|
1549
|
+
return undefined;
|
|
1550
|
+
}
|
|
1551
|
+
if (pattern.hostname !== undefined &&
|
|
1552
|
+
pattern.hostname !== urlObj.hostname.toLowerCase()) {
|
|
1553
|
+
return undefined;
|
|
1554
|
+
}
|
|
1555
|
+
if (pattern.hostname !== undefined) {
|
|
1556
|
+
const expectedPort = pattern.port ?? defaultPortForProtocol(pattern.protocol ?? urlObj.protocol);
|
|
1557
|
+
if (expectedPort !== urlObj.port)
|
|
1558
|
+
return undefined;
|
|
1559
|
+
}
|
|
1560
|
+
const prefix = pattern.pathnamePrefix;
|
|
1561
|
+
if (prefix === "/") {
|
|
1562
|
+
if (urlObj.pathname.startsWith("//"))
|
|
1563
|
+
return undefined;
|
|
1564
|
+
if (pattern.pathMode === "segments") {
|
|
1565
|
+
return isNonEmptyPathSegments(urlObj.pathname)
|
|
1566
|
+
? urlObj.pathname
|
|
1567
|
+
: undefined;
|
|
1568
|
+
}
|
|
1569
|
+
return urlObj.pathname;
|
|
1570
|
+
}
|
|
1571
|
+
if (urlObj.pathname === prefix) {
|
|
1572
|
+
return pattern.pathMode === "catch-all-required" ? undefined : "";
|
|
1573
|
+
}
|
|
1574
|
+
if (urlObj.pathname.startsWith(`${prefix}/`)) {
|
|
1575
|
+
const suffix = urlObj.pathname.slice(prefix.length);
|
|
1576
|
+
if (suffix.startsWith("//"))
|
|
1577
|
+
return undefined;
|
|
1578
|
+
if (pattern.pathMode === "segments" &&
|
|
1579
|
+
!isNonEmptyPathSegments(suffix)) {
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
return suffix;
|
|
1583
|
+
}
|
|
1584
|
+
return undefined;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* @param {string} pathname
|
|
1588
|
+
* @returns {boolean}
|
|
1589
|
+
*/
|
|
1590
|
+
function isNonEmptyPathSegments(pathname) {
|
|
1591
|
+
return pathname.length > 1 && !pathname.endsWith("/") && !pathname.includes("//");
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* URLPattern treats the default port as empty in URL instances.
|
|
1595
|
+
*
|
|
1596
|
+
* @param {string} protocol
|
|
1597
|
+
* @returns {string}
|
|
1598
|
+
*/
|
|
1599
|
+
function defaultPortForProtocol(protocol) {
|
|
1600
|
+
if (protocol === "http:")
|
|
1601
|
+
return "";
|
|
1602
|
+
if (protocol === "https:")
|
|
1603
|
+
return "";
|
|
1604
|
+
return "";
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* @param {FastPathPattern} pattern
|
|
1608
|
+
* @param {string} suffix
|
|
1609
|
+
* @param {URL} source
|
|
1610
|
+
* @returns {URL}
|
|
1611
|
+
*/
|
|
1612
|
+
function applyFastPathPattern(pattern, suffix, source) {
|
|
1613
|
+
const localized = new URL(source.href);
|
|
1614
|
+
if (pattern.protocol !== undefined)
|
|
1615
|
+
localized.protocol = pattern.protocol;
|
|
1616
|
+
if (pattern.hostname !== undefined) {
|
|
1617
|
+
localized.hostname = pattern.hostname;
|
|
1618
|
+
localized.port =
|
|
1619
|
+
pattern.port ?? defaultPortForProtocol(localized.protocol);
|
|
1620
|
+
}
|
|
1621
|
+
localized.pathname = joinFastPathPrefix(pattern.pathnamePrefix, suffix);
|
|
1622
|
+
return localized;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* @param {string} prefix
|
|
1626
|
+
* @param {string} suffix
|
|
1627
|
+
* @returns {string}
|
|
1628
|
+
*/
|
|
1629
|
+
function joinFastPathPrefix(prefix, suffix) {
|
|
1630
|
+
if (prefix === "/")
|
|
1631
|
+
return suffix === "" ? "/" : suffix;
|
|
1632
|
+
if (suffix === "")
|
|
1633
|
+
return prefix;
|
|
1634
|
+
return `${prefix}${suffix.startsWith("/") ? suffix : `/${suffix}`}`;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/** @type {string | undefined} */
|
|
1638
|
+
let cachedRouteStrategyUrl;
|
|
1639
|
+
/** @type {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined} */
|
|
1640
|
+
let cachedRouteStrategy;
|
|
1641
|
+
/**
|
|
1642
|
+
* Match route policy against both the public URL and its canonical URL.
|
|
1643
|
+
*
|
|
1644
|
+
* The function is deliberately separate from variables.js: configuration is
|
|
1645
|
+
* inert data, while canonicalization and route selection form a routing layer.
|
|
1646
|
+
*
|
|
1647
|
+
* @param {string | URL} url
|
|
1648
|
+
* @returns {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined}
|
|
1649
|
+
*/
|
|
1650
|
+
export function findMatchingRouteStrategy(url) {
|
|
1651
|
+
if (routeStrategies.length === 0) {
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1654
|
+
const urlString = typeof url === "string" ? url : url.href;
|
|
1655
|
+
if (cachedRouteStrategyUrl === urlString) {
|
|
1656
|
+
return cachedRouteStrategy;
|
|
1657
|
+
}
|
|
1658
|
+
const publicUrl = normalizeTrailingSlash(new URL(urlString, "http://example.com"));
|
|
1659
|
+
const canonicalUrl = deLocalizeUrl(publicUrl);
|
|
1660
|
+
const candidateUrls = canonicalUrl.href === publicUrl.href
|
|
1661
|
+
? [publicUrl]
|
|
1662
|
+
: [publicUrl, canonicalUrl];
|
|
1663
|
+
let match;
|
|
1664
|
+
for (const candidateUrl of candidateUrls) {
|
|
1665
|
+
for (const routeStrategy of routeStrategies) {
|
|
1666
|
+
const pattern = new URLPattern(routeStrategy.match, candidateUrl.href);
|
|
1667
|
+
if (execUrlPattern(pattern, candidateUrl)) {
|
|
1668
|
+
match = routeStrategy;
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (match)
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
cachedRouteStrategyUrl = urlString;
|
|
1676
|
+
cachedRouteStrategy = match;
|
|
1677
|
+
return match;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Returns the strategy to use for a specific URL.
|
|
1681
|
+
*
|
|
1682
|
+
* If no route strategy matches (or the matching rule is `exclude: true`),
|
|
1683
|
+
* the global strategy is returned.
|
|
1684
|
+
*
|
|
1685
|
+
* @param {string | URL} url
|
|
1686
|
+
* @returns {typeof strategy}
|
|
1687
|
+
*/
|
|
1688
|
+
export function getStrategyForUrl(url) {
|
|
1689
|
+
const routeStrategy = findMatchingRouteStrategy(url);
|
|
1690
|
+
if (routeStrategy &&
|
|
1691
|
+
routeStrategy.exclude !== true &&
|
|
1692
|
+
Array.isArray(routeStrategy.strategy)) {
|
|
1693
|
+
return routeStrategy.strategy;
|
|
1694
|
+
}
|
|
1695
|
+
return strategy;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Returns whether the given URL is excluded from middleware i18n processing.
|
|
1699
|
+
*
|
|
1700
|
+
* @param {string | URL} url
|
|
1701
|
+
* @returns {boolean}
|
|
1702
|
+
*/
|
|
1703
|
+
export function isExcludedByRouteStrategy(url) {
|
|
1704
|
+
return findMatchingRouteStrategy(url)?.exclude === true;
|
|
1705
|
+
}
|
|
1255
1706
|
|
|
1256
1707
|
/**
|
|
1257
1708
|
* @typedef {object} ShouldRedirectServerInput
|
|
@@ -1368,10 +1819,12 @@ async function resolveLocale(input, currentUrl) {
|
|
|
1368
1819
|
* @returns {URL}
|
|
1369
1820
|
*/
|
|
1370
1821
|
function resolveUrl(input) {
|
|
1371
|
-
if ("effectiveRequestUrl" in input &&
|
|
1822
|
+
if ("effectiveRequestUrl" in input &&
|
|
1823
|
+
input.effectiveRequestUrl instanceof URL) {
|
|
1372
1824
|
return new URL(input.effectiveRequestUrl.href);
|
|
1373
1825
|
}
|
|
1374
|
-
if ("effectiveRequestUrl" in input &&
|
|
1826
|
+
if ("effectiveRequestUrl" in input &&
|
|
1827
|
+
typeof input.effectiveRequestUrl === "string") {
|
|
1375
1828
|
return new URL(input.effectiveRequestUrl, input.request ? input.request.url : getUrlOrigin());
|
|
1376
1829
|
}
|
|
1377
1830
|
if (input.request) {
|
|
@@ -1396,7 +1849,9 @@ function resolveUrl(input) {
|
|
|
1396
1849
|
*/
|
|
1397
1850
|
function normalizeUrl(url) {
|
|
1398
1851
|
const urlObj = new URL(url);
|
|
1399
|
-
|
|
1852
|
+
if (trailingSlash === undefined) {
|
|
1853
|
+
urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
|
|
1854
|
+
}
|
|
1400
1855
|
return urlObj.href;
|
|
1401
1856
|
}
|
|
1402
1857
|
|
|
@@ -1574,9 +2029,12 @@ export function generateStaticLocalizedUrls(urls) {
|
|
|
1574
2029
|
// For default URL pattern, we can optimize the generation
|
|
1575
2030
|
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
1576
2031
|
for (const urlInput of urls) {
|
|
1577
|
-
const
|
|
2032
|
+
const originalUrl = urlInput instanceof URL
|
|
1578
2033
|
? urlInput
|
|
1579
2034
|
: new URL(urlInput, "http://localhost");
|
|
2035
|
+
const url = trailingSlash === undefined
|
|
2036
|
+
? originalUrl
|
|
2037
|
+
: normalizeTrailingSlash(new URL(originalUrl));
|
|
1580
2038
|
// Base locale doesn't get a prefix
|
|
1581
2039
|
localizedUrls.add(url);
|
|
1582
2040
|
// Other locales get their code as prefix
|
|
@@ -1584,7 +2042,7 @@ export function generateStaticLocalizedUrls(urls) {
|
|
|
1584
2042
|
if (locale !== baseLocale) {
|
|
1585
2043
|
const localizedPath = `/${locale}${url.pathname}${url.search}${url.hash}`;
|
|
1586
2044
|
const localizedUrl = new URL(localizedPath, url.origin);
|
|
1587
|
-
localizedUrls.add(localizedUrl);
|
|
2045
|
+
localizedUrls.add(normalizeTrailingSlash(localizedUrl));
|
|
1588
2046
|
}
|
|
1589
2047
|
}
|
|
1590
2048
|
}
|
|
@@ -1592,15 +2050,16 @@ export function generateStaticLocalizedUrls(urls) {
|
|
|
1592
2050
|
}
|
|
1593
2051
|
// For custom URL patterns, we need to use localizeUrl for each URL and locale
|
|
1594
2052
|
for (const urlInput of urls) {
|
|
1595
|
-
const
|
|
2053
|
+
const originalUrl = urlInput instanceof URL
|
|
1596
2054
|
? urlInput
|
|
1597
2055
|
: new URL(urlInput, "http://localhost");
|
|
2056
|
+
const url = normalizeTrailingSlash(new URL(originalUrl));
|
|
1598
2057
|
// Try each URL pattern to find one that matches
|
|
1599
2058
|
let patternFound = false;
|
|
1600
2059
|
for (const pattern of urlPatterns) {
|
|
1601
2060
|
try {
|
|
1602
2061
|
// Try to match the unlocalized pattern
|
|
1603
|
-
const unlocalizedMatch = new URLPattern(pattern.pattern, url.href)
|
|
2062
|
+
const unlocalizedMatch = execUrlPattern(new URLPattern(pattern.pattern, url.href), url);
|
|
1604
2063
|
if (!unlocalizedMatch)
|
|
1605
2064
|
continue;
|
|
1606
2065
|
patternFound = true;
|
|
@@ -1631,7 +2090,7 @@ export function generateStaticLocalizedUrls(urls) {
|
|
|
1631
2090
|
}
|
|
1632
2091
|
// If no pattern matched, use the URL as is
|
|
1633
2092
|
if (!patternFound) {
|
|
1634
|
-
localizedUrls.add(
|
|
2093
|
+
localizedUrls.add(originalUrl);
|
|
1635
2094
|
}
|
|
1636
2095
|
}
|
|
1637
2096
|
return Array.from(localizedUrls);
|