@mpgd/i18n 0.5.7 → 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.
Files changed (125) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.js +18 -9
  3. package/dist/paraglide/messages/_index.d.ts +59 -58
  4. package/dist/paraglide/messages/_index.js +6 -6
  5. package/dist/paraglide/messages/action_interstitial_ads.d.ts +4 -4
  6. package/dist/paraglide/messages/action_interstitial_ads.js +2 -2
  7. package/dist/paraglide/messages/action_leaderboard.d.ts +4 -4
  8. package/dist/paraglide/messages/action_leaderboard.js +2 -2
  9. package/dist/paraglide/messages/action_localization.d.ts +4 -4
  10. package/dist/paraglide/messages/action_localization.js +2 -2
  11. package/dist/paraglide/messages/action_purchases.d.ts +4 -4
  12. package/dist/paraglide/messages/action_purchases.js +2 -2
  13. package/dist/paraglide/messages/action_reward_ads.d.ts +4 -4
  14. package/dist/paraglide/messages/action_reward_ads.js +2 -2
  15. package/dist/paraglide/messages/app_title.d.ts +4 -4
  16. package/dist/paraglide/messages/app_title.js +2 -2
  17. package/dist/paraglide/messages/availability_off.d.ts +4 -4
  18. package/dist/paraglide/messages/availability_off.js +2 -2
  19. package/dist/paraglide/messages/availability_on.d.ts +4 -4
  20. package/dist/paraglide/messages/availability_on.js +2 -2
  21. package/dist/paraglide/messages/availability_unsupported.d.ts +4 -4
  22. package/dist/paraglide/messages/availability_unsupported.js +2 -2
  23. package/dist/paraglide/messages/cap_iap.d.ts +4 -4
  24. package/dist/paraglide/messages/cap_iap.js +2 -2
  25. package/dist/paraglide/messages/cap_leaderboard.d.ts +4 -4
  26. package/dist/paraglide/messages/cap_leaderboard.js +2 -2
  27. package/dist/paraglide/messages/cap_localization.d.ts +4 -4
  28. package/dist/paraglide/messages/cap_localization.js +2 -2
  29. package/dist/paraglide/messages/cap_rewarded_ads.d.ts +4 -4
  30. package/dist/paraglide/messages/cap_rewarded_ads.js +2 -2
  31. package/dist/paraglide/messages/cap_save.d.ts +4 -4
  32. package/dist/paraglide/messages/cap_save.js +2 -2
  33. package/dist/paraglide/messages/effective_config_summary.d.ts +8 -8
  34. package/dist/paraglide/messages/effective_config_summary.js +2 -2
  35. package/dist/paraglide/messages/feature_target_disabled.d.ts +6 -6
  36. package/dist/paraglide/messages/feature_target_disabled.js +2 -2
  37. package/dist/paraglide/messages/feature_unavailable.d.ts +6 -6
  38. package/dist/paraglide/messages/feature_unavailable.js +2 -2
  39. package/dist/paraglide/messages/feature_unsupported.d.ts +6 -6
  40. package/dist/paraglide/messages/feature_unsupported.js +2 -2
  41. package/dist/paraglide/messages/leaderboard_action.d.ts +4 -4
  42. package/dist/paraglide/messages/leaderboard_action.js +2 -2
  43. package/dist/paraglide/messages/leaderboard_action_unavailable.d.ts +4 -4
  44. package/dist/paraglide/messages/leaderboard_action_unavailable.js +2 -2
  45. package/dist/paraglide/messages/leaderboard_opened.d.ts +4 -4
  46. package/dist/paraglide/messages/leaderboard_opened.js +2 -2
  47. package/dist/paraglide/messages/leaderboard_unavailable.d.ts +4 -4
  48. package/dist/paraglide/messages/leaderboard_unavailable.js +2 -2
  49. package/dist/paraglide/messages/loading_player.d.ts +4 -4
  50. package/dist/paraglide/messages/loading_player.js +2 -2
  51. package/dist/paraglide/messages/mock_only.d.ts +4 -4
  52. package/dist/paraglide/messages/mock_only.js +2 -2
  53. package/dist/paraglide/messages/opening_purchase.d.ts +4 -4
  54. package/dist/paraglide/messages/opening_purchase.js +2 -2
  55. package/dist/paraglide/messages/play_again.d.ts +4 -4
  56. package/dist/paraglide/messages/play_again.js +2 -2
  57. package/dist/paraglide/messages/player.d.ts +6 -6
  58. package/dist/paraglide/messages/player.js +2 -2
  59. package/dist/paraglide/messages/preparing_demo.d.ts +4 -4
  60. package/dist/paraglide/messages/preparing_demo.js +2 -2
  61. package/dist/paraglide/messages/purchase_action.d.ts +4 -4
  62. package/dist/paraglide/messages/purchase_action.js +2 -2
  63. package/dist/paraglide/messages/purchase_completed.d.ts +4 -4
  64. package/dist/paraglide/messages/purchase_completed.js +2 -2
  65. package/dist/paraglide/messages/purchase_status.d.ts +6 -6
  66. package/dist/paraglide/messages/purchase_status.js +2 -2
  67. package/dist/paraglide/messages/purchase_unavailable.d.ts +4 -4
  68. package/dist/paraglide/messages/purchase_unavailable.js +2 -2
  69. package/dist/paraglide/messages/reward_ad_action.d.ts +4 -4
  70. package/dist/paraglide/messages/reward_ad_action.js +2 -2
  71. package/dist/paraglide/messages/reward_ad_unavailable.d.ts +4 -4
  72. package/dist/paraglide/messages/reward_ad_unavailable.js +2 -2
  73. package/dist/paraglide/messages/reward_granted.d.ts +4 -4
  74. package/dist/paraglide/messages/reward_granted.js +2 -2
  75. package/dist/paraglide/messages/reward_unavailable.d.ts +6 -6
  76. package/dist/paraglide/messages/reward_unavailable.js +2 -2
  77. package/dist/paraglide/messages/save_summary.d.ts +7 -7
  78. package/dist/paraglide/messages/save_summary.js +2 -2
  79. package/dist/paraglide/messages/saved.d.ts +4 -4
  80. package/dist/paraglide/messages/saved.js +2 -2
  81. package/dist/paraglide/messages/saved_and_submitted.d.ts +4 -4
  82. package/dist/paraglide/messages/saved_and_submitted.js +2 -2
  83. package/dist/paraglide/messages/saving_result.d.ts +4 -4
  84. package/dist/paraglide/messages/saving_result.js +2 -2
  85. package/dist/paraglide/messages/score.d.ts +6 -6
  86. package/dist/paraglide/messages/score.js +2 -2
  87. package/dist/paraglide/messages/sdk_summary.d.ts +6 -6
  88. package/dist/paraglide/messages/sdk_summary.js +2 -2
  89. package/dist/paraglide/messages/showing_rewarded_ad.d.ts +4 -4
  90. package/dist/paraglide/messages/showing_rewarded_ad.js +2 -2
  91. package/dist/paraglide/messages/status_cleared.d.ts +4 -4
  92. package/dist/paraglide/messages/status_cleared.js +2 -2
  93. package/dist/paraglide/messages/status_try_again.d.ts +4 -4
  94. package/dist/paraglide/messages/status_try_again.js +2 -2
  95. package/dist/paraglide/messages/tap_to_start.d.ts +4 -4
  96. package/dist/paraglide/messages/tap_to_start.js +2 -2
  97. package/dist/paraglide/messages/target.d.ts +6 -6
  98. package/dist/paraglide/messages/target.js +2 -2
  99. package/dist/paraglide/messages/target_availability_summary.d.ts +7 -7
  100. package/dist/paraglide/messages/target_availability_summary.js +2 -2
  101. package/dist/paraglide/messages/target_config_unavailable.d.ts +4 -4
  102. package/dist/paraglide/messages/target_config_unavailable.js +2 -2
  103. package/dist/paraglide/messages/target_feature_iap.d.ts +4 -4
  104. package/dist/paraglide/messages/target_feature_iap.js +2 -2
  105. package/dist/paraglide/messages/target_feature_interstitial_ads.d.ts +4 -4
  106. package/dist/paraglide/messages/target_feature_interstitial_ads.js +2 -2
  107. package/dist/paraglide/messages/target_feature_leaderboard.d.ts +4 -4
  108. package/dist/paraglide/messages/target_feature_leaderboard.js +2 -2
  109. package/dist/paraglide/messages/target_feature_localization.d.ts +4 -4
  110. package/dist/paraglide/messages/target_feature_localization.js +2 -2
  111. package/dist/paraglide/messages/target_feature_rewarded_ads.d.ts +4 -4
  112. package/dist/paraglide/messages/target_feature_rewarded_ads.js +2 -2
  113. package/dist/paraglide/messages/viewport.d.ts +8 -8
  114. package/dist/paraglide/messages/viewport.js +2 -2
  115. package/dist/paraglide/messages/viewport_orientation_mismatch.d.ts +7 -7
  116. package/dist/paraglide/messages/viewport_orientation_mismatch.js +2 -2
  117. package/dist/paraglide/messages/viewport_orientation_policy.d.ts +6 -6
  118. package/dist/paraglide/messages/viewport_orientation_policy.js +2 -2
  119. package/dist/paraglide/messages.d.ts +2 -2
  120. package/dist/paraglide/registry.d.ts +5 -5
  121. package/dist/paraglide/runtime.d.ts +548 -283
  122. package/dist/paraglide/runtime.js +545 -86
  123. package/dist/paraglide/server.d.ts +1 -1
  124. package/dist/paraglide/server.js +20 -8
  125. package/package.json +4 -4
@@ -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
- * Returns whether the given URL is excluded from middleware i18n processing.
73
+ * Controls trailing slash canonicalization for localized URLs.
121
74
  *
122
- * @param {string | URL} url
123
- * @returns {boolean}
75
+ * @type {"always" | "never" | undefined}
124
76
  */
125
- export function isExcludedByRouteStrategy(url) {
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 reloads the page on the client to reflect the new locale. Reloading
396
- * can be disabled by passing `reload: false` as an option, but you'll need to ensure
397
- * the UI updates to reflect the new locale.
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://y.com" in non-browser environments. If this
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
- result = defaultUrlPatternExtractLocale(url);
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).exec(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://dummy.com");
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 urlObj = typeof url === "string" ? new URL(url) : url;
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 = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
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 = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
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 urlObj;
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 urlObj = typeof url === "string" ? new URL(url) : url;
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 = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
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 = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
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 urlObj;
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 && input.effectiveRequestUrl instanceof URL) {
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 && typeof input.effectiveRequestUrl === "string") {
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
- urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
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 url = urlInput instanceof URL
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 url = urlInput instanceof URL
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).exec(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(url);
2093
+ localizedUrls.add(originalUrl);
1635
2094
  }
1636
2095
  }
1637
2096
  return Array.from(localizedUrls);