@turnipxenon/pineapple 5.3.15 → 5.3.17

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 (33) hide show
  1. package/dist/external/paraglide/.prettierignore +1 -1
  2. package/dist/external/paraglide/README.md +162 -0
  3. package/dist/external/paraglide/messages/_index.d.ts +3 -8
  4. package/dist/external/paraglide/messages/_index.d.ts.map +1 -1
  5. package/dist/external/paraglide/messages/_index.js +3 -50
  6. package/dist/external/paraglide/messages/example_message.d.ts +19 -0
  7. package/dist/external/paraglide/messages/example_message.d.ts.map +1 -0
  8. package/dist/external/paraglide/messages/example_message.js +34 -0
  9. package/dist/external/paraglide/messages/package.json +4 -0
  10. package/dist/external/paraglide/messages/settings.d.ts +17 -0
  11. package/dist/external/paraglide/messages/settings.d.ts.map +1 -0
  12. package/dist/external/paraglide/messages/settings.js +33 -0
  13. package/dist/external/paraglide/registry.d.ts +13 -0
  14. package/dist/external/paraglide/registry.d.ts.map +1 -1
  15. package/dist/external/paraglide/registry.js +15 -0
  16. package/dist/external/paraglide/runtime.d.ts +340 -139
  17. package/dist/external/paraglide/runtime.d.ts.map +1 -1
  18. package/dist/external/paraglide/runtime.js +654 -168
  19. package/dist/external/paraglide/server.d.ts +47 -18
  20. package/dist/external/paraglide/server.d.ts.map +1 -1
  21. package/dist/external/paraglide/server.js +150 -35
  22. package/dist/modules/parsnip/external-images/externalImages.remote.d.ts +2 -2
  23. package/dist/modules/parsnip/external-images/externalImages.remote.d.ts.map +1 -1
  24. package/package.json +26 -26
  25. package/dist/external/paraglide/messages/en.d.ts +0 -5
  26. package/dist/external/paraglide/messages/en.d.ts.map +0 -1
  27. package/dist/external/paraglide/messages/en.js +0 -10
  28. package/dist/external/paraglide/messages/fr.d.ts +0 -5
  29. package/dist/external/paraglide/messages/fr.d.ts.map +0 -1
  30. package/dist/external/paraglide/messages/fr.js +0 -10
  31. package/dist/external/paraglide/messages/tl.d.ts +0 -5
  32. package/dist/external/paraglide/messages/tl.d.ts.map +0 -1
  33. package/dist/external/paraglide/messages/tl.js +0 -7
@@ -20,7 +20,7 @@ export const baseLocale = "en";
20
20
  * throw new Error('Locale is not available');
21
21
  * }
22
22
  */
23
- export const locales = /** @type {const} */ (["en", "fr", "tl"]);
23
+ export const locales = /** @type {const} */ (["en","fr","tl"]);
24
24
  /** @type {string} */
25
25
  export const cookieName = "PARAGLIDE_LOCALE";
26
26
  /** @type {number} */
@@ -33,14 +33,26 @@ export const localStorageKey = "PARAGLIDE_LOCALE";
33
33
  * @type {Array<"cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage" | "localStorage" | `custom-${string}`>}
34
34
  */
35
35
  export const strategy = [
36
- "url",
37
36
  "cookie",
37
+ "globalVariable",
38
38
  "baseLocale"
39
39
  ];
40
+ /**
41
+ * Route-level strategy overrides.
42
+ *
43
+ * `match` uses URLPattern syntax.
44
+ *
45
+ * @type {Array<{
46
+ * match: string;
47
+ * strategy?: Array<"cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage" | "localStorage" | `custom-${string}`>;
48
+ * exclude?: boolean;
49
+ * }>}
50
+ */
51
+ export const routeStrategies = [];
40
52
  /**
41
53
  * The used URL patterns.
42
54
  *
43
- * @type {Array<{ pattern: string, localized: Array<[Locale, string]> }> }
55
+ * @type {Array<{ pattern: string, localized: Array<[Locale, string]> }>}
44
56
  */
45
57
  export const urlPatterns = [
46
58
  {
@@ -81,9 +93,22 @@ export const urlPatterns = [
81
93
  * @type {ParaglideAsyncLocalStorage | undefined}
82
94
  */
83
95
  export let serverAsyncLocalStorage = undefined;
96
+ /**
97
+ * Returns the current server-side async local storage instance.
98
+ *
99
+ * Accessing the mutable value through a function keeps it observable when
100
+ * module interceptors wrap exported bindings and snapshot their initial value.
101
+ *
102
+ * @returns {ParaglideAsyncLocalStorage | undefined}
103
+ */
104
+ export function getServerAsyncLocalStorage() {
105
+ return serverAsyncLocalStorage;
106
+ }
84
107
  export const disableAsyncLocalStorage = false;
85
108
  export const experimentalMiddlewareLocaleSplitting = false;
86
- export const isServer = import.meta.env?.SSR ?? typeof window === 'undefined';
109
+ export const isServer = typeof window === 'undefined';
110
+ /** @type {Locale | undefined} */
111
+ export const experimentalStaticLocale = undefined;
87
112
  /**
88
113
  * Sets the server side async local storage.
89
114
  *
@@ -98,28 +123,37 @@ export function overwriteServerAsyncLocalStorage(value) {
98
123
  serverAsyncLocalStorage = value;
99
124
  }
100
125
  const TREE_SHAKE_COOKIE_STRATEGY_USED = true;
101
- const TREE_SHAKE_URL_STRATEGY_USED = true;
102
- const TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED = false;
126
+ const TREE_SHAKE_URL_STRATEGY_USED = false;
127
+ const TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED = true;
103
128
  const TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED = false;
104
129
  const TREE_SHAKE_DEFAULT_URL_PATTERN_USED = true;
105
130
  const TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED = false;
106
131
 
107
- globalThis.__paraglide = {}
132
+ /** @type {any} */ (globalThis).__paraglide =
133
+ /** @type {any} */ (globalThis).__paraglide ?? {};
134
+ /** @type {any} */ (globalThis).__paraglide.ssr =
135
+ /** @type {any} */ (globalThis).__paraglide.ssr ?? {};
108
136
 
109
137
  /**
110
138
  * This is a fallback to get started with a custom
111
139
  * strategy and avoid type errors.
112
140
  *
113
141
  * The implementation is overwritten
114
- * by \`overwriteGetLocale()\` and \`defineSetLocale()\`.
142
+ * by `overwriteGetLocale()` and `defineSetLocale()`.
115
143
  *
116
- * @type {Locale|undefined}
144
+ * @type {Locale | undefined}
117
145
  */
118
146
  let _locale;
119
147
  let localeInitiallySet = false;
120
148
  /**
121
149
  * Get the current locale.
122
150
  *
151
+ * The locale is resolved using your configured strategies (URL, cookie, localStorage, etc.)
152
+ * in the order they are defined. In SSR contexts, the locale is retrieved from AsyncLocalStorage
153
+ * which is set by the `paraglideMiddleware()`.
154
+ *
155
+ * @see https://paraglidejs.com/strategy - Configure locale detection strategies
156
+ *
123
157
  * @example
124
158
  * if (getLocale() === 'de') {
125
159
  * console.log('Germany 🇩🇪');
@@ -127,11 +161,12 @@ let localeInitiallySet = false;
127
161
  * console.log('Netherlands 🇳🇱');
128
162
  * }
129
163
  *
130
- * @type {() => Locale}
164
+ * @returns {Locale} The current locale.
131
165
  */
132
166
  export let getLocale = () => {
133
- /** @type {string | undefined} */
134
- let locale;
167
+ if (experimentalStaticLocale !== undefined) {
168
+ return experimentalStaticLocale;
169
+ }
135
170
  // if running in a server-side rendering context
136
171
  // retrieve the locale from the async local storage
137
172
  if (serverAsyncLocalStorage) {
@@ -140,7 +175,48 @@ export let getLocale = () => {
140
175
  return locale;
141
176
  }
142
177
  }
143
- for (const strat of strategy) {
178
+ let strategyToUse = strategy;
179
+ if (!isServer && typeof window !== "undefined" && window.location?.href) {
180
+ strategyToUse = getStrategyForUrl(window.location.href);
181
+ }
182
+ const resolved = resolveLocaleWithStrategies(strategyToUse, typeof window !== "undefined" ? window.location?.href : undefined);
183
+ if (resolved) {
184
+ if (!localeInitiallySet) {
185
+ _locale = resolved;
186
+ // https://github.com/opral/inlang-paraglide-js/issues/455
187
+ localeInitiallySet = true;
188
+ setLocale(resolved, { reload: false });
189
+ }
190
+ return resolved;
191
+ }
192
+ throw new Error("No locale found. Read the docs https://paraglidejs.com/errors#no-locale-found");
193
+ };
194
+ /**
195
+ * Resolve locale for a given URL using route-aware strategies.
196
+ *
197
+ * @param {string | URL} url
198
+ * @returns {Locale}
199
+ */
200
+ export function getLocaleForUrl(url) {
201
+ if (experimentalStaticLocale !== undefined) {
202
+ return experimentalStaticLocale;
203
+ }
204
+ const strategyToUse = getStrategyForUrl(url);
205
+ const resolved = resolveLocaleWithStrategies(strategyToUse, typeof url === "string" ? url : url.href);
206
+ if (resolved) {
207
+ return resolved;
208
+ }
209
+ throw new Error("No locale found. Read the docs https://paraglidejs.com/errors#no-locale-found");
210
+ }
211
+ /**
212
+ * @param {typeof strategy} strategyToUse
213
+ * @param {string | undefined} urlForUrlStrategy
214
+ * @returns {Locale | undefined}
215
+ */
216
+ function resolveLocaleWithStrategies(strategyToUse, urlForUrlStrategy) {
217
+ /** @type {string | undefined} */
218
+ let locale;
219
+ for (const strat of strategyToUse) {
144
220
  if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
145
221
  locale = extractLocaleFromCookie();
146
222
  }
@@ -150,8 +226,8 @@ export let getLocale = () => {
150
226
  else if (TREE_SHAKE_URL_STRATEGY_USED &&
151
227
  strat === "url" &&
152
228
  !isServer &&
153
- typeof window !== "undefined") {
154
- locale = extractLocaleFromUrl(window.location.href);
229
+ typeof urlForUrlStrategy === "string") {
230
+ locale = extractLocaleFromUrl(urlForUrlStrategy);
155
231
  }
156
232
  else if (TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED &&
157
233
  strat === "globalVariable" &&
@@ -177,47 +253,86 @@ export let getLocale = () => {
177
253
  // Can't await in sync function, skip async strategies
178
254
  continue;
179
255
  }
180
- locale = result;
256
+ if (result !== undefined) {
257
+ return assertIsLocale(result);
258
+ }
181
259
  }
182
260
  }
183
- // check if match, else continue loop
184
- if (locale !== undefined) {
185
- const asserted = assertIsLocale(locale);
186
- if (!localeInitiallySet) {
187
- _locale = asserted;
188
- // https://github.com/opral/inlang-paraglide-js/issues/455
189
- localeInitiallySet = true;
190
- setLocale(asserted, { reload: false });
191
- }
192
- return asserted;
261
+ const matchedLocale = toLocale(locale);
262
+ if (matchedLocale) {
263
+ return matchedLocale;
193
264
  }
194
265
  }
195
- throw new Error("No locale found. Read the docs https://inlang.com/m/gerre34r/library-inlang-paraglideJs/errors#no-locale-found");
196
- };
266
+ return undefined;
267
+ }
197
268
  /**
198
- * Overwrite the \`getLocale()\` function.
269
+ * Overwrite the `getLocale()` function.
199
270
  *
200
- * Use this function to overwrite how the locale is resolved. For example,
201
- * you can resolve the locale from the browser's preferred language,
202
- * a cookie, env variable, or a user's preference.
271
+ * Use this function to overwrite how the locale is resolved. This is useful
272
+ * for custom locale resolution or advanced use cases like SSG with concurrent rendering.
273
+ *
274
+ * @see https://paraglidejs.com/strategy
203
275
  *
204
276
  * @example
205
277
  * overwriteGetLocale(() => {
206
- * // resolve the locale from a cookie. fallback to the base locale.
207
278
  * return Cookies.get('locale') ?? baseLocale
208
- * }
279
+ * });
209
280
  *
210
- * @type {(fn: () => Locale) => void}
281
+ * @param {() => Locale} fn - The new implementation for `getLocale()`.
211
282
  */
212
283
  export const overwriteGetLocale = (fn) => {
213
284
  getLocale = fn;
214
285
  };
215
286
 
287
+ const rtlLanguages = new Set([
288
+ "ar",
289
+ "dv",
290
+ "fa",
291
+ "he",
292
+ "ks",
293
+ "ku",
294
+ "ps",
295
+ "sd",
296
+ "ug",
297
+ "ur",
298
+ "yi",
299
+ ]);
300
+ /**
301
+ * Get writing direction for a locale.
302
+ *
303
+ * Uses `Intl.Locale` text info when available and falls back to a
304
+ * language-based RTL check for runtimes without `getTextInfo()`.
305
+ *
306
+ * @example
307
+ * getTextDirection(); // "ltr" or "rtl" for current locale
308
+ * getTextDirection("ar"); // "rtl"
309
+ * getTextDirection("en"); // "ltr"
310
+ *
311
+ * @param {string} [locale] - Target locale. If not provided, uses `getLocale()`
312
+ * @returns {"ltr" | "rtl"}
313
+ */
314
+ export function getTextDirection(locale = getLocale()) {
315
+ try {
316
+ const intlLocale = /** @type {Intl.Locale & {
317
+ getTextInfo?: () => { direction?: string };
318
+ textInfo?: { direction?: string };
319
+ }} */ (new Intl.Locale(locale));
320
+ const direction = intlLocale.getTextInfo?.().direction ?? intlLocale.textInfo?.direction;
321
+ if (direction === "ltr" || direction === "rtl") {
322
+ return direction;
323
+ }
324
+ }
325
+ catch {
326
+ // Ignore Intl.Locale parsing/runtime errors and use fallback below.
327
+ }
328
+ const language = locale.split("-")[0]?.toLowerCase();
329
+ return rtlLanguages.has(language ?? "") ? "rtl" : "ltr";
330
+ }
331
+
216
332
  /**
217
333
  * Navigates to the localized URL, or reloads the current page
218
334
  *
219
335
  * @param {string} [newLocation] The new location
220
- * @return {undefined}
221
336
  */
222
337
  const navigateOrReload = (newLocation) => {
223
338
  if (newLocation) {
@@ -235,13 +350,18 @@ const navigateOrReload = (newLocation) => {
235
350
  /**
236
351
  * Set the locale.
237
352
  *
238
- * Set locale reloads the site by default on the client. Reloading
239
- * can be disabled by passing \`reload: false\` as an option. If
240
- * reloading is disabled, you need to ensure that the UI is updated
241
- * to reflect the new locale.
353
+ * Updates the locale using your configured strategies (cookie, localStorage, URL, etc.).
354
+ * By default, this navigates the client to the localized URL or reloads the current
355
+ * document to reflect the new locale. `reload: false` is a narrow browser-only escape
356
+ * hatch for a fully client-rendered, non-URL-routed surface that owns its reactive
357
+ * updates and document state. It does not re-render the UI or update the document.
358
+ * Do not use it for normal locale pickers, URL-routed pages, or switching an SSR,
359
+ * SSG, or hydrated document. It is incompatible with per-locale builds.
242
360
  *
243
- * If any custom strategy's \`setLocale\` function is async, then this
244
- * function will become async as well.
361
+ * If any custom strategy's `setLocale` function is async, then this function
362
+ * will become async as well.
363
+ *
364
+ * @see https://paraglidejs.com/strategy
245
365
  *
246
366
  * @example
247
367
  * setLocale('en');
@@ -256,6 +376,12 @@ export let setLocale = (newLocale, options) => {
256
376
  reload: true,
257
377
  ...options,
258
378
  };
379
+ if (experimentalStaticLocale !== undefined &&
380
+ newLocale !== experimentalStaticLocale &&
381
+ optionsWithDefaults.reload === false) {
382
+ 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.`);
383
+ optionsWithDefaults.reload = true;
384
+ }
259
385
  // locale is already set
260
386
  // https://github.com/opral/inlang-paraglide-js/issues/430
261
387
  /** @type {Locale | undefined} */
@@ -266,11 +392,15 @@ export let setLocale = (newLocale, options) => {
266
392
  catch {
267
393
  // do nothing, no locale has been set yet.
268
394
  }
269
- /** @type {Array<Promise<any>>} */
395
+ /** @type {Array<Promise<void>>} */
270
396
  const customSetLocalePromises = [];
271
397
  /** @type {string | undefined} */
272
398
  let newLocation = undefined;
273
- for (const strat of strategy) {
399
+ let strategyToUse = strategy;
400
+ if (!isServer && typeof window !== "undefined" && window.location?.href) {
401
+ strategyToUse = getStrategyForUrl(window.location.href);
402
+ }
403
+ for (const strat of strategyToUse) {
274
404
  if (TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED &&
275
405
  strat === "globalVariable") {
276
406
  // a default for a custom strategy to get started quickly
@@ -288,6 +418,7 @@ export let setLocale = (newLocale, options) => {
288
418
  document.cookie = cookieDomain
289
419
  ? `${cookieString}; domain=${cookieDomain}`
290
420
  : cookieString;
421
+ clearLocaleCookieCache();
291
422
  }
292
423
  else if (strat === "baseLocale") {
293
424
  // nothing to be set here. baseLocale is only a fallback
@@ -347,7 +478,7 @@ export let setLocale = (newLocale, options) => {
347
478
  return;
348
479
  };
349
480
  /**
350
- * Overwrite the \`setLocale()\` function.
481
+ * Overwrite the `setLocale()` function.
351
482
  *
352
483
  * Use this function to overwrite how the locale is set. For example,
353
484
  * modify a cookie, env variable, or a user's preference.
@@ -361,13 +492,13 @@ export let setLocale = (newLocale, options) => {
361
492
  * @param {SetLocaleFn} fn
362
493
  */
363
494
  export const overwriteSetLocale = (fn) => {
364
- setLocale = /** @type {SetLocaleFn} */ (fn);
495
+ setLocale = fn;
365
496
  };
366
497
 
367
498
  /**
368
499
  * The origin of the current URL.
369
500
  *
370
- * Defaults to "http://y.com" in non-browser environments. If this
501
+ * Defaults to "http://example.com" in non-browser environments. If this
371
502
  * behavior is not desired, the implementation can be overwritten
372
503
  * by `overwriteGetUrlOrigin()`.
373
504
  *
@@ -388,14 +519,32 @@ export let getUrlOrigin = () => {
388
519
  * Use this function in server environments to
389
520
  * define how the URL origin is resolved.
390
521
  *
391
- * @type {(fn: () => string) => void}
522
+ * @param {() => string} fn - The new implementation for `getUrlOrigin()`.
392
523
  */
393
524
  export let overwriteGetUrlOrigin = (fn) => {
394
525
  getUrlOrigin = fn;
395
526
  };
396
527
 
397
528
  /**
398
- * Check if something is an available locale.
529
+ * Coerces a locale-like string to the canonical locale value used by the runtime.
530
+ *
531
+ * @param {unknown} value
532
+ * @returns {Locale | undefined}
533
+ */
534
+ export function toLocale(value) {
535
+ if (typeof value !== "string") {
536
+ return undefined;
537
+ }
538
+ const lowerValue = value.toLowerCase();
539
+ for (const locale of locales) {
540
+ if (locale.toLowerCase() === lowerValue) {
541
+ return locale;
542
+ }
543
+ }
544
+ return undefined;
545
+ }
546
+ /**
547
+ * Check if something is an available locale with the canonical project casing.
399
548
  *
400
549
  * @example
401
550
  * if (isLocale(params.locale)) {
@@ -404,27 +553,32 @@ export let overwriteGetUrlOrigin = (fn) => {
404
553
  * setLocale('en');
405
554
  * }
406
555
  *
407
- * @param {any} locale
556
+ * Use `toLocale()` when you want case-insensitive matching and canonicalization.
557
+ *
558
+ * @param {unknown} locale
408
559
  * @returns {locale is Locale}
409
560
  */
410
561
  export function isLocale(locale) {
411
- return !locale ? false : locales.includes(locale);
562
+ return !!locale && locales.some((item) => item === locale);
412
563
  }
413
-
414
564
  /**
415
- * Asserts that the input is a locale.
565
+ * Asserts that the input can be normalized to a locale.
416
566
  *
417
- * @param {any} input - The input to check.
418
- * @returns {Locale} The input if it is a locale.
567
+ * @param {unknown} input - The input to check.
568
+ * @returns {Locale} The input normalized to a Locale.
419
569
  * @throws {Error} If the input is not a locale.
420
570
  */
421
571
  export function assertIsLocale(input) {
422
- if (isLocale(input) === false) {
423
- throw new Error(`Invalid locale: ${input}. Expected one of: ${locales.join(", ")}`);
424
- }
425
- return input;
572
+ const locale = toLocale(input);
573
+ if (locale)
574
+ return locale;
575
+ throw new Error(`Invalid locale: ${input}. Expected one of: ${locales.join(", ")}`);
426
576
  }
427
577
 
578
+ /**
579
+ * @typedef {object} ExtractLocaleFromRequestOptions
580
+ * @property {string | URL} [effectiveRequestUrl] - Effective request URL to use for route matching and locale detection with the URL strategy.
581
+ */
428
582
  /**
429
583
  * Extracts a locale from a request.
430
584
  *
@@ -441,21 +595,38 @@ export function assertIsLocale(input) {
441
595
  * @example
442
596
  * const locale = extractLocaleFromRequest(request);
443
597
  *
444
- * @type {(request: Request) => Locale}
598
+ * @param {Request} request
599
+ * @param {ExtractLocaleFromRequestOptions} [options]
600
+ * @returns {Locale}
445
601
  */
446
- export const extractLocaleFromRequest = (request) => {
602
+ export const extractLocaleFromRequest = (request, options = {}) => {
603
+ const effectiveRequestUrl = resolveEffectiveRequestUrl(request, options.effectiveRequestUrl);
604
+ return extractLocaleFromRequestWithStrategies(request, getStrategyForUrl(effectiveRequestUrl), effectiveRequestUrl);
605
+ };
606
+ /**
607
+ * Extracts a locale from a request using the provided strategy order.
608
+ *
609
+ * @param {Request} request
610
+ * @param {typeof strategy} strategies
611
+ * @param {string | URL} [url]
612
+ * @returns {Locale}
613
+ */
614
+ export const extractLocaleFromRequestWithStrategies = (request, strategies, url = request.url) => {
615
+ const effectiveRequestUrl = resolveEffectiveRequestUrl(request, url);
447
616
  /** @type {string|undefined} */
448
617
  let locale;
449
- for (const strat of strategy) {
618
+ for (const strat of strategies) {
450
619
  if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
620
+ const cookiePrefix = cookieName + "=";
451
621
  locale = request.headers
452
622
  .get("cookie")
453
- ?.split("; ")
454
- .find((c) => c.startsWith(cookieName + "="))
455
- ?.split("=")[1];
623
+ ?.split(";")
624
+ .map((c) => c.trim())
625
+ .find((c) => c.startsWith(cookiePrefix))
626
+ ?.slice(cookiePrefix.length);
456
627
  }
457
628
  else if (TREE_SHAKE_URL_STRATEGY_USED && strat === "url") {
458
- locale = extractLocaleFromUrl(request.url);
629
+ locale = extractLocaleFromUrl(effectiveRequestUrl);
459
630
  }
460
631
  else if (TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED &&
461
632
  strat === "preferredLanguage") {
@@ -475,17 +646,24 @@ export const extractLocaleFromRequest = (request) => {
475
646
  // Use extractLocaleFromRequestAsync for custom server strategies
476
647
  continue;
477
648
  }
478
- if (locale !== undefined) {
479
- if (!isLocale(locale)) {
480
- locale = undefined;
481
- }
482
- else {
483
- return assertIsLocale(locale);
484
- }
649
+ const matchedLocale = toLocale(locale);
650
+ if (matchedLocale) {
651
+ return matchedLocale;
485
652
  }
486
653
  }
487
- throw new Error("No locale found. There is an error in your strategy. Try adding 'baseLocale' as the very last strategy. Read more here https://inlang.com/m/gerre34r/library-inlang-paraglideJs/errors#no-locale-found");
654
+ throw new Error("No locale found. There is an error in your strategy. Try adding 'baseLocale' as the very last strategy. Read more here https://paraglidejs.com/errors#no-locale-found");
488
655
  };
656
+ /**
657
+ * @param {Request} request
658
+ * @param {string | URL | undefined} effectiveRequestUrl
659
+ * @returns {URL}
660
+ */
661
+ function resolveEffectiveRequestUrl(request, effectiveRequestUrl = request.url) {
662
+ if (effectiveRequestUrl instanceof URL) {
663
+ return new URL(effectiveRequestUrl.href);
664
+ }
665
+ return new URL(effectiveRequestUrl, request.url);
666
+ }
489
667
 
490
668
  /**
491
669
  * Asynchronously extracts a locale from a request.
@@ -514,11 +692,15 @@ export const extractLocaleFromRequest = (request) => {
514
692
  *
515
693
  * const locale = await extractLocaleFromRequestAsync(request);
516
694
  *
517
- * @type {(request: Request) => Promise<Locale>}
695
+ * @param {Request} request - The request object to extract the locale from.
696
+ * @param {{ effectiveRequestUrl?: string | URL }} [options] - Effective request URL to use for route matching and locale detection with the URL strategy.
697
+ * @returns {Promise<Locale>} The extracted locale.
518
698
  */
519
- export const extractLocaleFromRequestAsync = async (request) => {
699
+ export const extractLocaleFromRequestAsync = async (request, options = {}) => {
520
700
  /** @type {string|undefined} */
521
701
  let locale;
702
+ const effectiveRequestUrl = resolveEffectiveRequestUrlFromRequestAsync(request, options.effectiveRequestUrl);
703
+ const strategy = getStrategyForUrl(effectiveRequestUrl);
522
704
  // Process custom strategies first, in order
523
705
  for (const strat of strategy) {
524
706
  if (isCustomStrategy(strat) && customServerStrategies.has(strat)) {
@@ -528,34 +710,66 @@ export const extractLocaleFromRequestAsync = async (request) => {
528
710
  locale = await handler.getLocale(request);
529
711
  }
530
712
  // If we got a valid locale from this custom strategy, use it
531
- if (locale !== undefined && isLocale(locale)) {
532
- return assertIsLocale(locale);
713
+ const matchedLocale = toLocale(locale);
714
+ if (matchedLocale) {
715
+ return matchedLocale;
533
716
  }
534
717
  }
535
718
  }
536
719
  // If no custom strategy provided a valid locale, fall back to sync version
537
- locale = extractLocaleFromRequest(request);
538
- return assertIsLocale(locale);
720
+ return extractLocaleFromRequestWithStrategies(request, strategy, effectiveRequestUrl);
539
721
  };
722
+ /**
723
+ * @param {Request} request
724
+ * @param {string | URL | undefined} effectiveRequestUrl
725
+ * @returns {URL}
726
+ */
727
+ function resolveEffectiveRequestUrlFromRequestAsync(request, effectiveRequestUrl = request.url) {
728
+ if (effectiveRequestUrl instanceof URL) {
729
+ return new URL(effectiveRequestUrl.href);
730
+ }
731
+ return new URL(effectiveRequestUrl, request.url);
732
+ }
540
733
 
734
+ const cookieNamePattern = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
735
+ const localeCookiePattern = new RegExp(`(?:^|;\\s*)${cookieNamePattern}=([^;]*)`);
736
+ const noCachedLocale = Symbol();
737
+ /** @type {Locale | undefined | typeof noCachedLocale} */
738
+ let cachedLocaleFromCookie = noCachedLocale;
739
+ /**
740
+ * Clears the cached locale from `document.cookie`.
741
+ */
742
+ function clearLocaleCookieCache() {
743
+ cachedLocaleFromCookie = noCachedLocale;
744
+ }
745
+ function scheduleLocaleCookieCacheClear() {
746
+ if (typeof queueMicrotask === "function") {
747
+ queueMicrotask(clearLocaleCookieCache);
748
+ }
749
+ else {
750
+ Promise.resolve().then(clearLocaleCookieCache);
751
+ }
752
+ }
541
753
  /**
542
754
  * Extracts a cookie from the document.
543
755
  *
544
756
  * Will return undefined if the document is not available or if the cookie is not set.
545
757
  * The `document` object is not available in server-side rendering, so this function should not be called in that context.
546
758
  *
547
- * @returns {string | undefined}
759
+ * @returns {Locale | undefined}
548
760
  */
549
761
  export function extractLocaleFromCookie() {
550
- if (typeof document === "undefined" || !document.cookie) {
762
+ if (typeof document === "undefined") {
551
763
  return;
552
764
  }
553
- const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
554
- const locale = match?.[2];
555
- if (isLocale(locale)) {
556
- return locale;
765
+ if (cachedLocaleFromCookie !== noCachedLocale) {
766
+ return cachedLocaleFromCookie;
557
767
  }
558
- return undefined;
768
+ const match = document.cookie.match(localeCookiePattern);
769
+ const locale = match?.[1];
770
+ cachedLocaleFromCookie = toLocale(locale);
771
+ scheduleLocaleCookieCacheClear();
772
+ return cachedLocaleFromCookie;
559
773
  }
560
774
 
561
775
  /**
@@ -567,9 +781,8 @@ export function extractLocaleFromCookie() {
567
781
  * @example
568
782
  * const locale = extractLocaleFromHeader(request);
569
783
  *
570
- * @type {(request: Request) => Locale}
571
784
  * @param {Request} request - The request object to extract the locale from.
572
- * @returns {string|undefined} The negotiated preferred language.
785
+ * @returns {Locale | undefined} The negotiated preferred language.
573
786
  */
574
787
  export function extractLocaleFromHeader(request) {
575
788
  const acceptLanguageHeader = request.headers.get("accept-language");
@@ -580,20 +793,22 @@ export function extractLocaleFromHeader(request) {
580
793
  .map((lang) => {
581
794
  const [tag, q = "1"] = lang.trim().split(";q=");
582
795
  // Get both the full tag and base language code
583
- const baseTag = tag?.split("-")[0]?.toLowerCase();
796
+ const baseTag = tag?.split("-")[0];
584
797
  return {
585
- fullTag: tag?.toLowerCase(),
798
+ fullTag: tag,
586
799
  baseTag,
587
800
  q: Number(q),
588
801
  };
589
802
  })
590
803
  .sort((a, b) => b.q - a.q);
591
804
  for (const lang of languages) {
592
- if (isLocale(lang.fullTag)) {
593
- return lang.fullTag;
805
+ const fullLocale = toLocale(lang.fullTag);
806
+ if (fullLocale) {
807
+ return fullLocale;
594
808
  }
595
- else if (isLocale(lang.baseTag)) {
596
- return lang.baseTag;
809
+ const baseLocale = toLocale(lang.baseTag);
810
+ if (baseLocale) {
811
+ return baseLocale;
597
812
  }
598
813
  }
599
814
  return undefined;
@@ -610,23 +825,24 @@ export function extractLocaleFromHeader(request) {
610
825
  * @example
611
826
  * const locale = extractLocaleFromNavigator();
612
827
  *
613
- * @type {() => Locale | undefined}
614
- * @returns {string | undefined}
828
+ * @returns {Locale | undefined}
615
829
  */
616
830
  export function extractLocaleFromNavigator() {
617
831
  if (!navigator?.languages?.length) {
618
832
  return undefined;
619
833
  }
620
834
  const languages = navigator.languages.map((lang) => ({
621
- fullTag: lang.toLowerCase(),
622
- baseTag: lang.split("-")[0]?.toLowerCase(),
835
+ fullTag: lang,
836
+ baseTag: lang.split("-")[0],
623
837
  }));
624
838
  for (const lang of languages) {
625
- if (isLocale(lang.fullTag)) {
626
- return lang.fullTag;
839
+ const fullLocale = toLocale(lang.fullTag);
840
+ if (fullLocale) {
841
+ return fullLocale;
627
842
  }
628
- else if (isLocale(lang.baseTag)) {
629
- return lang.baseTag;
843
+ const baseLocale = toLocale(lang.baseTag);
844
+ if (baseLocale) {
845
+ return baseLocale;
630
846
  }
631
847
  }
632
848
  return undefined;
@@ -645,6 +861,10 @@ let cachedLocale;
645
861
  /**
646
862
  * Extracts the locale from a given URL using native URLPattern.
647
863
  *
864
+ * The built-in default `/:locale/...` routing is case-insensitive because it
865
+ * canonicalizes the first path segment with `toLocale()`. Custom `urlPatterns`
866
+ * keep URLPattern's normal exact matching semantics for path segments.
867
+ *
648
868
  * @param {URL|string} url - The full URL from which to extract the locale.
649
869
  * @returns {Locale|undefined} The extracted locale, or undefined if no locale is found.
650
870
  */
@@ -653,6 +873,7 @@ export function extractLocaleFromUrl(url) {
653
873
  if (cachedUrl === urlString) {
654
874
  return cachedLocale;
655
875
  }
876
+ /** @type {Locale | undefined} */
656
877
  let result;
657
878
  if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
658
879
  result = defaultUrlPatternExtractLocale(url);
@@ -663,11 +884,7 @@ export function extractLocaleFromUrl(url) {
663
884
  for (const element of urlPatterns) {
664
885
  for (const [locale, localizedPattern] of element.localized) {
665
886
  const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
666
- if (!match) {
667
- continue;
668
- }
669
- // Check if the locale is valid
670
- if (assertIsLocale(locale)) {
887
+ if (match) {
671
888
  result = locale;
672
889
  break;
673
890
  }
@@ -683,20 +900,13 @@ export function extractLocaleFromUrl(url) {
683
900
  /**
684
901
  * https://github.com/opral/inlang-paraglide-js/issues/381
685
902
  *
686
- * @param {URL|string} url - The full URL from which to extract the locale.
687
- * @returns {Locale|undefined} The extracted locale, or undefined if no locale is found.
903
+ * @param {URL | string} url - The full URL from which to extract the locale.
904
+ * @returns {Locale | undefined} The extracted locale, or undefined if no locale is found.
688
905
  */
689
906
  function defaultUrlPatternExtractLocale(url) {
690
- const urlObj = new URL(url, "http://dummy.com");
907
+ const urlObj = new URL(url, "http://example.com");
691
908
  const pathSegments = urlObj.pathname.split("/").filter(Boolean);
692
- if (pathSegments.length > 0) {
693
- const potentialLocale = pathSegments[0];
694
- if (isLocale(potentialLocale)) {
695
- return potentialLocale;
696
- }
697
- }
698
- // everything else has to be the base locale
699
- return baseLocale;
909
+ return toLocale(pathSegments[0]) || baseLocale;
700
910
  }
701
911
 
702
912
  /**
@@ -709,6 +919,8 @@ function defaultUrlPatternExtractLocale(url) {
709
919
  * For client-side UI components, use `localizeHref()` instead, which provides
710
920
  * a more convenient API with relative paths and automatic locale detection.
711
921
  *
922
+ * @see https://paraglidejs.com/i18n-routing
923
+ *
712
924
  * @example
713
925
  * ```typescript
714
926
  * // Server middleware example
@@ -737,15 +949,17 @@ function defaultUrlPatternExtractLocale(url) {
737
949
  * ```
738
950
  *
739
951
  * @param {string | URL} url - The URL to localize. If string, must be absolute.
740
- * @param {Object} [options] - Options for localization
741
- * @param {string} [options.locale] - Target locale. If not provided, uses getLocale()
952
+ * @param {object} [options] - Options for localization
953
+ * @param {Locale} [options.locale] - Target locale. If not provided, uses getLocale()
742
954
  * @returns {URL} The localized URL, always absolute
743
955
  */
744
956
  export function localizeUrl(url, options) {
957
+ const targetLocale = options?.locale
958
+ ? assertIsLocale(options?.locale)
959
+ : getLocale();
745
960
  if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
746
- return localizeUrlDefaultPattern(url, options);
961
+ return localizeUrlDefaultPattern(url, targetLocale);
747
962
  }
748
- const targetLocale = options?.locale ?? getLocale();
749
963
  const urlObj = typeof url === "string" ? new URL(url) : url;
750
964
  // Iterate over URL patterns
751
965
  for (const element of urlPatterns) {
@@ -778,13 +992,11 @@ export function localizeUrl(url, options) {
778
992
  * https://github.com/opral/inlang-paraglide-js/issues/381
779
993
  *
780
994
  * @param {string | URL} url
781
- * @param {Object} [options]
782
- * @param {string} [options.locale]
995
+ * @param {Locale} locale
783
996
  * @returns {URL}
784
997
  */
785
- function localizeUrlDefaultPattern(url, options) {
998
+ function localizeUrlDefaultPattern(url, locale) {
786
999
  const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
787
- const locale = options?.locale ?? getLocale();
788
1000
  const currentLocale = extractLocaleFromUrl(urlObj);
789
1001
  // If current locale matches target locale, no change needed
790
1002
  if (currentLocale === locale) {
@@ -792,7 +1004,7 @@ function localizeUrlDefaultPattern(url, options) {
792
1004
  }
793
1005
  const pathSegments = urlObj.pathname.split("/").filter(Boolean);
794
1006
  // If current path starts with a locale, remove it
795
- if (pathSegments.length > 0 && isLocale(pathSegments[0])) {
1007
+ if (pathSegments.length > 0 && toLocale(pathSegments[0])) {
796
1008
  pathSegments.shift();
797
1009
  }
798
1010
  // For base locale, don't add prefix
@@ -815,6 +1027,8 @@ function localizeUrlDefaultPattern(url, options) {
815
1027
  * For client-side UI components, use `deLocalizeHref()` instead, which provides
816
1028
  * a more convenient API with relative paths.
817
1029
  *
1030
+ * @see https://paraglidejs.com/i18n-routing
1031
+ *
818
1032
  * @example
819
1033
  * ```typescript
820
1034
  * // Server middleware example
@@ -878,7 +1092,7 @@ function deLocalizeUrlDefaultPattern(url) {
878
1092
  const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
879
1093
  const pathSegments = urlObj.pathname.split("/").filter(Boolean);
880
1094
  // If first segment is a locale, remove it
881
- if (pathSegments.length > 0 && isLocale(pathSegments[0])) {
1095
+ if (pathSegments.length > 0 && toLocale(pathSegments[0])) {
882
1096
  urlObj.pathname = "/" + pathSegments.slice(1).join("/");
883
1097
  }
884
1098
  return urlObj;
@@ -991,7 +1205,8 @@ function fillPattern(pattern, values, origin) {
991
1205
  * Aggregates named groups from various parts of the URLPattern match result.
992
1206
  *
993
1207
  *
994
- * @type {(match: any) => Record<string, string | null | undefined>}
1208
+ * @param {any} match - The URLPattern match result object.
1209
+ * @returns {Record<string, string | null | undefined>} An object containing all named groups from the match.
995
1210
  */
996
1211
  export function aggregateGroups(match) {
997
1212
  return {
@@ -1006,22 +1221,92 @@ export function aggregateGroups(match) {
1006
1221
  };
1007
1222
  }
1008
1223
 
1224
+ /** @type {string | undefined} */
1225
+ let cachedRouteStrategyUrl;
1226
+ /** @type {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined} */
1227
+ let cachedRouteStrategy;
1228
+ /**
1229
+ * Match route policy against both the public URL and its canonical URL.
1230
+ *
1231
+ * The function is deliberately separate from variables.js: configuration is
1232
+ * inert data, while canonicalization and route selection form a routing layer.
1233
+ *
1234
+ * @param {string | URL} url
1235
+ * @returns {{ match: string; strategy?: typeof strategy; exclude?: boolean } | undefined}
1236
+ */
1237
+ export function findMatchingRouteStrategy(url) {
1238
+ if (routeStrategies.length === 0) {
1239
+ return undefined;
1240
+ }
1241
+ const urlString = typeof url === "string" ? url : url.href;
1242
+ if (cachedRouteStrategyUrl === urlString) {
1243
+ return cachedRouteStrategy;
1244
+ }
1245
+ const publicUrl = new URL(urlString, "http://example.com");
1246
+ const canonicalUrl = deLocalizeUrl(publicUrl);
1247
+ const candidateUrls = canonicalUrl.href === publicUrl.href
1248
+ ? [publicUrl]
1249
+ : [publicUrl, canonicalUrl];
1250
+ let match;
1251
+ for (const candidateUrl of candidateUrls) {
1252
+ for (const routeStrategy of routeStrategies) {
1253
+ const pattern = new URLPattern(routeStrategy.match, candidateUrl.href);
1254
+ if (pattern.exec(candidateUrl.href)) {
1255
+ match = routeStrategy;
1256
+ break;
1257
+ }
1258
+ }
1259
+ if (match)
1260
+ break;
1261
+ }
1262
+ cachedRouteStrategyUrl = urlString;
1263
+ cachedRouteStrategy = match;
1264
+ return match;
1265
+ }
1266
+ /**
1267
+ * Returns the strategy to use for a specific URL.
1268
+ *
1269
+ * If no route strategy matches (or the matching rule is `exclude: true`),
1270
+ * the global strategy is returned.
1271
+ *
1272
+ * @param {string | URL} url
1273
+ * @returns {typeof strategy}
1274
+ */
1275
+ export function getStrategyForUrl(url) {
1276
+ const routeStrategy = findMatchingRouteStrategy(url);
1277
+ if (routeStrategy &&
1278
+ routeStrategy.exclude !== true &&
1279
+ Array.isArray(routeStrategy.strategy)) {
1280
+ return routeStrategy.strategy;
1281
+ }
1282
+ return strategy;
1283
+ }
1284
+ /**
1285
+ * Returns whether the given URL is excluded from middleware i18n processing.
1286
+ *
1287
+ * @param {string | URL} url
1288
+ * @returns {boolean}
1289
+ */
1290
+ export function isExcludedByRouteStrategy(url) {
1291
+ return findMatchingRouteStrategy(url)?.exclude === true;
1292
+ }
1293
+
1009
1294
  /**
1010
1295
  * @typedef {object} ShouldRedirectServerInput
1011
1296
  * @property {Request} request
1012
- * @property {string | URL} [url]
1013
- * @property {ReturnType<typeof assertIsLocale>} [locale]
1297
+ * @property {string | URL} [effectiveRequestUrl] - Effective request URL to use for route matching, locale detection with the URL strategy, and redirect targets.
1298
+ * @property {Locale} [locale]
1014
1299
  *
1015
1300
  * @typedef {object} ShouldRedirectClientInput
1016
1301
  * @property {undefined} [request]
1017
1302
  * @property {string | URL} [url]
1018
- * @property {ReturnType<typeof assertIsLocale>} [locale]
1303
+ * @property {Locale} [locale]
1019
1304
  *
1020
1305
  * @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
1021
1306
  *
1022
1307
  * @typedef {object} ShouldRedirectResult
1023
1308
  * @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
1024
- * @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
1309
+ * @property {Locale} locale - Locale resolved using the configured strategies.
1025
1310
  * @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
1026
1311
  */
1027
1312
  /**
@@ -1033,6 +1318,8 @@ export function aggregateGroups(match) {
1033
1318
  *
1034
1319
  * When called in the browser without arguments, the current `window.location.href` is used.
1035
1320
  *
1321
+ * @see https://paraglidejs.com/i18n-routing#redirects
1322
+ *
1036
1323
  * @example
1037
1324
  * // Client side usage (e.g. TanStack Router beforeLoad hook)
1038
1325
  * async function beforeLoad({ location }) {
@@ -1055,15 +1342,33 @@ export function aggregateGroups(match) {
1055
1342
  * return render(request, decision.locale);
1056
1343
  * }
1057
1344
  *
1345
+ * @example
1346
+ * // Server side usage behind a proxy where request.url is not public-facing
1347
+ * export async function handle(request) {
1348
+ * const effectiveRequestUrl = new URL(request.url);
1349
+ * effectiveRequestUrl.protocol = "https:";
1350
+ * effectiveRequestUrl.host = "example.com";
1351
+ *
1352
+ * const decision = await shouldRedirect({
1353
+ * request,
1354
+ * effectiveRequestUrl,
1355
+ * });
1356
+ *
1357
+ * if (decision.shouldRedirect) {
1358
+ * return Response.redirect(decision.redirectUrl, 307);
1359
+ * }
1360
+ * }
1361
+ *
1058
1362
  * @param {ShouldRedirectInput} [input]
1059
1363
  * @returns {Promise<ShouldRedirectResult>}
1060
1364
  */
1061
1365
  export async function shouldRedirect(input = {}) {
1062
- const locale = /** @type {ReturnType<typeof assertIsLocale>} */ (await resolveLocale(input));
1063
- if (!strategy.includes("url")) {
1366
+ const currentUrl = resolveUrl(input);
1367
+ const locale = await resolveLocale(input, currentUrl);
1368
+ const strategy = getStrategyForUrl(currentUrl.href);
1369
+ if (isExcludedByRouteStrategy(currentUrl.href) || !strategy.includes("url")) {
1064
1370
  return { shouldRedirect: false, locale, redirectUrl: undefined };
1065
1371
  }
1066
- const currentUrl = resolveUrl(input);
1067
1372
  const localizedUrl = localizeUrl(currentUrl.href, { locale });
1068
1373
  const shouldRedirectToLocalizedUrl = normalizeUrl(localizedUrl.href) !== normalizeUrl(currentUrl.href);
1069
1374
  return {
@@ -1076,14 +1381,21 @@ export async function shouldRedirect(input = {}) {
1076
1381
  * Resolves the locale either from the provided input or by using the configured strategies.
1077
1382
  *
1078
1383
  * @param {ShouldRedirectInput} input
1079
- * @returns {Promise<ReturnType<typeof assertIsLocale>>}
1384
+ * @param {URL} currentUrl
1385
+ * @returns {Promise<Locale>}
1080
1386
  */
1081
- async function resolveLocale(input) {
1082
- if (input.locale) {
1083
- return assertIsLocale(input.locale);
1387
+ async function resolveLocale(input, currentUrl) {
1388
+ const locale = toLocale(input.locale);
1389
+ if (locale) {
1390
+ return locale;
1084
1391
  }
1085
1392
  if (input.request) {
1086
- return extractLocaleFromRequestAsync(input.request);
1393
+ return extractLocaleFromRequestAsync(input.request, {
1394
+ effectiveRequestUrl: currentUrl,
1395
+ });
1396
+ }
1397
+ if ("url" in input && typeof input.url !== "undefined") {
1398
+ return getLocaleForUrl(currentUrl.href);
1087
1399
  }
1088
1400
  return getLocale();
1089
1401
  }
@@ -1094,13 +1406,21 @@ async function resolveLocale(input) {
1094
1406
  * @returns {URL}
1095
1407
  */
1096
1408
  function resolveUrl(input) {
1409
+ if ("effectiveRequestUrl" in input &&
1410
+ input.effectiveRequestUrl instanceof URL) {
1411
+ return new URL(input.effectiveRequestUrl.href);
1412
+ }
1413
+ if ("effectiveRequestUrl" in input &&
1414
+ typeof input.effectiveRequestUrl === "string") {
1415
+ return new URL(input.effectiveRequestUrl, input.request ? input.request.url : getUrlOrigin());
1416
+ }
1097
1417
  if (input.request) {
1098
1418
  return new URL(input.request.url);
1099
1419
  }
1100
- if (input.url instanceof URL) {
1420
+ if ("url" in input && input.url instanceof URL) {
1101
1421
  return new URL(input.url.href);
1102
1422
  }
1103
- if (typeof input.url === "string") {
1423
+ if ("url" in input && typeof input.url === "string") {
1104
1424
  return new URL(input.url, getUrlOrigin());
1105
1425
  }
1106
1426
  if (typeof window !== "undefined" && window?.location?.href) {
@@ -1131,6 +1451,8 @@ function normalizeUrl(url) {
1131
1451
  * - Automatically detects current locale if not specified
1132
1452
  * - Handles string input/output instead of URL objects
1133
1453
  *
1454
+ * @see https://paraglidejs.com/i18n-routing
1455
+ *
1134
1456
  * @example
1135
1457
  * ```typescript
1136
1458
  * // In a React/Vue/Svelte component
@@ -1154,8 +1476,8 @@ function normalizeUrl(url) {
1154
1476
  * which provides more precise control over URL handling.
1155
1477
  *
1156
1478
  * @param {string} href - The href to localize (can be relative or absolute)
1157
- * @param {Object} [options] - Options for localization
1158
- * @param {string} [options.locale] - Target locale. If not provided, uses `getLocale()`
1479
+ * @param {object} [options] - Options for localization
1480
+ * @param {Locale} [options.locale] - Target locale. If not provided, uses `getLocale()`
1159
1481
  * @returns {string} The localized href, relative if input was relative
1160
1482
  */
1161
1483
  export function localizeHref(href, options) {
@@ -1189,6 +1511,8 @@ export function localizeHref(href, options) {
1189
1511
  * - Returns relative paths when possible
1190
1512
  * - Handles string input/output instead of URL objects
1191
1513
  *
1514
+ * @see https://paraglidejs.com/i18n-routing
1515
+ *
1192
1516
  * @example
1193
1517
  * ```typescript
1194
1518
  * // In a React/Vue/Svelte component
@@ -1216,7 +1540,6 @@ export function localizeHref(href, options) {
1216
1540
  *
1217
1541
  * @param {string} href - The href to de-localize (can be relative or absolute)
1218
1542
  * @returns {string} The de-localized href, relative if input was relative
1219
- * @see deLocalizeUrl - For low-level URL de-localization in server contexts
1220
1543
  */
1221
1544
  export function deLocalizeHref(href) {
1222
1545
  const url = new URL(href, getUrlOrigin());
@@ -1243,28 +1566,50 @@ export function trackMessageCall(safeModuleId, locale) {
1243
1566
  }
1244
1567
 
1245
1568
  /**
1246
- * Generates a list of localized URLs for all provided URLs.
1569
+ * Generates localized URL variants for all provided URLs based on your configured locales and URL patterns.
1570
+ *
1571
+ * This function is essential for Static Site Generation (SSG) where you need to tell your framework
1572
+ * which pages to pre-render at build time. It's also useful for generating sitemaps and
1573
+ * `<link rel="alternate" hreflang>` tags for SEO.
1574
+ *
1575
+ * The function respects your `urlPatterns` configuration - if you have translated pathnames
1576
+ * (e.g., `/about` → `/ueber-uns` for German), it will generate the correct localized paths.
1247
1577
  *
1248
- * This is useful for SSG (Static Site Generation) and sitemap generation.
1249
- * NextJS and other frameworks use this function for SSG.
1578
+ * @see https://paraglidejs.com/static-site-generation
1250
1579
  *
1251
1580
  * @example
1252
- * ```typescript
1253
- * const urls = generateStaticLocalizedUrls([
1254
- * "https://example.com/about",
1255
- * "https://example.com/blog",
1581
+ * // Basic usage - generate all locale variants for a list of paths
1582
+ * const localizedUrls = generateStaticLocalizedUrls([
1583
+ * "/",
1584
+ * "/about",
1585
+ * "/blog/post-1",
1256
1586
  * ]);
1257
- * urls[0].href // => "https://example.com/about"
1258
- * urls[1].href // => "https://example.com/blog"
1259
- * urls[2].href // => "https://example.com/de/about"
1260
- * urls[3].href // => "https://example.com/de/blog"
1261
- * ...
1262
- * ```
1587
+ * // Returns URL objects for each locale:
1588
+ * // ["/en/", "/de/", "/en/about", "/de/about", "/en/blog/post-1", "/de/blog/post-1"]
1589
+ *
1590
+ * @example
1591
+ * // Use with framework SSG APIs
1592
+ * // SvelteKit
1593
+ * export function entries() {
1594
+ * const paths = ["/", "/about", "/contact"];
1595
+ * return generateStaticLocalizedUrls(paths).map(url => ({
1596
+ * locale: extractLocaleFromUrl(url)
1597
+ * }));
1598
+ * }
1263
1599
  *
1264
- * @param {(string | URL)[]} urls - List of URLs to generate localized versions for. Can be absolute URLs or paths.
1265
- * @returns {URL[]} List of localized URLs as URL objects
1600
+ * @example
1601
+ * // Sitemap generation
1602
+ * const allPages = ["/", "/about", "/blog"];
1603
+ * const sitemapUrls = generateStaticLocalizedUrls(allPages);
1604
+ *
1605
+ * @param {(string | URL)[]} urls - List of canonical URLs or paths to generate localized versions for.
1606
+ * Can be absolute URLs (`https://example.com/about`) or paths (`/about`).
1607
+ * Paths are resolved against `http://localhost` internally.
1608
+ * @returns {URL[]} Array of URL objects representing all localized variants.
1609
+ * The order follows each input URL with all its locale variants before moving to the next URL.
1266
1610
  */
1267
1611
  export function generateStaticLocalizedUrls(urls) {
1612
+ /** @type {Set<URL>} */
1268
1613
  const localizedUrls = new Set();
1269
1614
  // For default URL pattern, we can optimize the generation
1270
1615
  if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
@@ -1357,7 +1702,7 @@ export const customClientStrategies = new Map();
1357
1702
  /**
1358
1703
  * Checks if the given strategy is a custom strategy.
1359
1704
  *
1360
- * @param {any} strategy The name of the custom strategy to validate.
1705
+ * @param {unknown} strategy The name of the custom strategy to validate.
1361
1706
  * Must be a string that starts with "custom-" followed by alphanumeric characters, hyphens, or underscores.
1362
1707
  * @returns {boolean} Returns true if it is a custom strategy, false otherwise.
1363
1708
  */
@@ -1367,7 +1712,9 @@ export function isCustomStrategy(strategy) {
1367
1712
  /**
1368
1713
  * Defines a custom strategy that is executed on the server.
1369
1714
  *
1370
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
1715
+ * @see https://paraglidejs.com/strategy#write-your-own-strategy
1716
+ *
1717
+ * @param {string} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
1371
1718
  * @param {CustomServerStrategyHandler} handler The handler for the custom strategy, which should implement
1372
1719
  * the method getLocale.
1373
1720
  * @returns {void}
@@ -1381,7 +1728,9 @@ export function defineCustomServerStrategy(strategy, handler) {
1381
1728
  /**
1382
1729
  * Defines a custom strategy that is executed on the client.
1383
1730
  *
1384
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
1731
+ * @see https://paraglidejs.com/strategy#write-your-own-strategy
1732
+ *
1733
+ * @param {string} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
1385
1734
  * @param {CustomClientStrategyHandler} handler The handler for the custom strategy, which should implement the
1386
1735
  * methods getLocale and setLocale.
1387
1736
  * @returns {void}
@@ -1394,13 +1743,150 @@ export function defineCustomClientStrategy(strategy, handler) {
1394
1743
  }
1395
1744
 
1396
1745
  // ------ TYPES ------
1397
-
1746
+ export {};
1398
1747
  /**
1399
1748
  * A locale that is available in the project.
1400
1749
  *
1401
1750
  * @example
1402
1751
  * setLocale(request.locale as Locale)
1403
1752
  *
1404
- * @typedef {(typeof locales)[number]} Locale
1753
+ * @typedef {typeof locales[number]} Locale
1754
+ */
1755
+ /**
1756
+ * A branded type representing a localized string.
1757
+ *
1758
+ * Message functions return this type instead of \`string\`, enabling TypeScript
1759
+ * to distinguish translated strings from regular strings at compile time.
1760
+ * This allows you to enforce that only properly localized content is used
1761
+ * in your UI components.
1762
+ *
1763
+ * Since \`LocalizedString\` is a branded subtype of \`string\`, it remains fully
1764
+ * backward compatible—you can pass it anywhere a \`string\` is expected.
1765
+ *
1766
+ * @example
1767
+ * // Enforce localized strings in your components
1768
+ * function PageTitle(props: { title: LocalizedString }) {
1769
+ * return <h1>{props.title}</h1>
1770
+ * }
1771
+ *
1772
+ * // ✅ Correct: using a message function
1773
+ * <PageTitle title={m.welcome_title()} />
1774
+ *
1775
+ * // ❌ Type error: raw strings are not LocalizedString
1776
+ * <PageTitle title="Welcome" />
1777
+ *
1778
+ * @example
1779
+ * // LocalizedString is assignable to string (backward compatible)
1780
+ * const localized: LocalizedString = m.greeting()
1781
+ * const str: string = localized // ✅ works fine
1782
+ *
1783
+ * // But string is not assignable to LocalizedString
1784
+ * const raw: LocalizedString = "Hello" // ❌ Type error
1785
+ *
1786
+ * @example
1787
+ * // Catches accidental string concatenation
1788
+ * function showMessage(msg: LocalizedString) { ... }
1789
+ *
1790
+ * showMessage(m.hello()) // ✅
1791
+ * showMessage("Hello " + userName) // ❌ Type error
1792
+ * showMessage(m.hello_user({ name: userName })) // ✅ use params instead
1793
+ *
1794
+ * @typedef {string & { readonly __brand: 'LocalizedString' }} LocalizedString
1795
+ */
1796
+ /**
1797
+ * A single markup option passed to a tag instance.
1798
+ *
1799
+ * @typedef {{
1800
+ * name: string;
1801
+ * value: unknown;
1802
+ * }} MessageMarkupOption
1803
+ */
1804
+ /**
1805
+ * A single static markup attribute attached to a tag instance.
1806
+ *
1807
+ * @typedef {{
1808
+ * name: string;
1809
+ * value: string | true;
1810
+ * }} MessageMarkupAttribute
1811
+ */
1812
+ /**
1813
+ * Record of markup options for a tag instance.
1814
+ *
1815
+ * @typedef {Record<string, unknown>} MessageMarkupOptions
1816
+ */
1817
+ /**
1818
+ * Record of markup attributes for a tag instance.
1819
+ *
1820
+ * @typedef {Record<string, string | true>} MessageMarkupAttributes
1821
+ */
1822
+ /**
1823
+ * Type-level schema for a single markup tag.
1824
+ *
1825
+ * @typedef {{
1826
+ * options: MessageMarkupOptions;
1827
+ * attributes: MessageMarkupAttributes;
1828
+ * children: boolean;
1829
+ * }} MessageMarkupTag
1830
+ */
1831
+ /**
1832
+ * Type-level schema for all markup tags in a message.
1833
+ *
1834
+ * @typedef {Record<string, MessageMarkupTag>} MessageMarkupSchema
1835
+ */
1836
+ /**
1837
+ * Type-only metadata attached to compiled message functions.
1838
+ *
1839
+ * @template Inputs
1840
+ * @template Options
1841
+ * @template {MessageMarkupSchema} [Markup = MessageMarkupSchema]
1842
+ * @typedef {{
1843
+ * readonly __paraglide?: {
1844
+ * inputs: Inputs;
1845
+ * options: Options;
1846
+ * markup: Markup;
1847
+ * };
1848
+ * }} MessageMetadata
1849
+ */
1850
+ /**
1851
+ * A compiled, framework-neutral message part.
1852
+ *
1853
+ * @typedef {{
1854
+ * type: "text";
1855
+ * value: string;
1856
+ * } | {
1857
+ * type: "markup-start";
1858
+ * name: string;
1859
+ * options: MessageMarkupOptions;
1860
+ * attributes: MessageMarkupAttributes;
1861
+ * } | {
1862
+ * type: "markup-end";
1863
+ * name: string;
1864
+ * options: MessageMarkupOptions;
1865
+ * attributes: MessageMarkupAttributes;
1866
+ * } | {
1867
+ * type: "markup-standalone";
1868
+ * name: string;
1869
+ * options: MessageMarkupOptions;
1870
+ * attributes: MessageMarkupAttributes;
1871
+ * }} MessagePart
1872
+ */
1873
+ /**
1874
+ * A message function is a message for a specific locale.
1875
+ *
1876
+ * @example
1877
+ * m.hello({ name: 'world' })
1878
+ *
1879
+ * @typedef {(inputs?: Record<string, never>) => LocalizedString} MessageFunction
1880
+ */
1881
+ /**
1882
+ * A message bundle function that selects the message to be returned.
1883
+ *
1884
+ * Uses `getLocale()` under the hood to determine the locale with an option.
1885
+ *
1886
+ * @template {string} T
1887
+ *
1888
+ * @example
1889
+ * * m.hello({ name: 'world' }, { locale: "en" })
1890
+ *
1891
+ * @typedef {(params: Record<string, never>, options: { locale: T }) => LocalizedString} MessageBundleFunction
1405
1892
  */
1406
-