@uniweb/runtime 1.1.18 → 1.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/runtime",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "Minimal runtime for loading Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -36,7 +36,7 @@
36
36
  "node": ">=22.12"
37
37
  },
38
38
  "dependencies": {
39
- "@uniweb/core": "~1.4.3",
39
+ "@uniweb/core": "~1.4.4",
40
40
  "@uniweb/theming": "~1.2.1"
41
41
  },
42
42
  "devDependencies": {
@@ -44,7 +44,7 @@
44
44
  "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0 || ^0.28.0",
45
45
  "vite": "^7.3.1",
46
46
  "vitest": "^4.1.7",
47
- "@uniweb/build": "1.8.2"
47
+ "@uniweb/build": "1.8.4"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": "^19.0.0",
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { useEffect, useCallback } from 'react'
16
16
  import { useNavigate, useLocation } from 'react-router-dom'
17
+ import { localeOfPath } from '@uniweb/core/resolve-route'
17
18
  import { prefetchContent, prefersReducedMotion } from '../setup.js'
18
19
 
19
20
  /**
@@ -74,6 +75,24 @@ function findAnchorElement(target) {
74
75
  return null
75
76
  }
76
77
 
78
+ /**
79
+ * The locale and page a link goes to when it leaves the active locale, or null.
80
+ *
81
+ * Which locale a path names is the hosts' rule (`localeOfPath`): a served locale's prefix,
82
+ * else the locale served unprefixed. The page is the route that locale shows, made canonical.
83
+ * Only a path names a locale: a fragment or query of this page does not.
84
+ *
85
+ * @param {string} routeHref - The link's href, base path removed
86
+ * @param {Object} website - The active Website
87
+ * @returns {{ locale: string, route: string } | null}
88
+ */
89
+ export function localeSwitchOf(routeHref, website) {
90
+ if (!website?.hasMultipleLocales() || !routeHref.startsWith('/') || routeHref.startsWith('//')) return null
91
+ const { locale, path } = localeOfPath(routeHref.split(/[?#]/)[0], website.config)
92
+ if (locale === website.getActiveLocale()) return null
93
+ return { locale, route: website.reverseTranslateRoute(path, locale) }
94
+ }
95
+
77
96
  /**
78
97
  * Scroll to element by ID with retry logic
79
98
  * Retries a few times to handle elements that render asynchronously
@@ -144,9 +163,6 @@ export function useLinkInterceptor(options = {}) {
144
163
  // Check for download attribute
145
164
  if (anchor.hasAttribute('download')) return
146
165
 
147
- // Opt out: anchors marked for full-page reload (e.g., <Link reload>)
148
- if (anchor.hasAttribute('data-reload')) return
149
-
150
166
  // Check for target="_blank" or other non-self targets
151
167
  const target = anchor.getAttribute('target')
152
168
  if (target && target !== '_self') return
@@ -162,21 +178,28 @@ export function useLinkInterceptor(options = {}) {
162
178
  routeHref = href.slice(basePath.length) || '/'
163
179
  }
164
180
 
165
- // Check if this link switches locale (requires full page reload)
166
- if (website?.hasMultipleLocales()) {
167
- const activeLocale = website.getActiveLocale()
168
- const defaultLocale = website.getDefaultLocale()
169
- const localeCodes = website.getLocales().map(l => l.code)
170
- const hrefMatch = routeHref.match(/^\/([a-z]{2,3}(?:-[A-Za-z]{2,4})?)(?:\/|$)/)
171
- const hrefLocale = hrefMatch?.[1]
172
-
173
- let isLocaleSwitch = false
174
- if (hrefLocale && localeCodes.includes(hrefLocale)) {
175
- isLocaleSwitch = hrefLocale !== activeLocale
176
- } else {
177
- // No locale prefix = targets default locale
178
- isLocaleSwitch = activeLocale !== defaultLocale
181
+ // A link to another locale's page is a page load, since that locale is its own document,
182
+ // and the browser makes it. A host that switches locale its own way (`uniweb.localeSwitch`:
183
+ // an editor's preview, which a page load would leave) is handed it instead, a
184
+ // `<Link reload>` from a language menu and a plain link to `/fr/…` alike.
185
+ if (globalThis.uniweb?.localeSwitch) {
186
+ const switchTo = localeSwitchOf(routeHref, website)
187
+ if (switchTo) {
188
+ event.preventDefault()
189
+ website.switchLocale(switchTo.locale, switchTo.route)
190
+ return
179
191
  }
192
+ }
193
+
194
+ // Opt out: anchors marked for full-page reload (e.g., <Link reload>)
195
+ if (anchor.hasAttribute('data-reload')) return
196
+
197
+ // Check if this link switches locale (requires full page reload). Any href that is not
198
+ // a path counts as the default locale's, as it always has.
199
+ if (website?.hasMultipleLocales()) {
200
+ const isLocaleSwitch = routeHref.startsWith('/') && !routeHref.startsWith('//')
201
+ ? !!localeSwitchOf(routeHref, website)
202
+ : website.getActiveLocale() !== website.getDefaultLocale()
180
203
  if (isLocaleSwitch) return // Allow full page reload
181
204
  }
182
205
 
package/src/setup.js CHANGED
@@ -269,9 +269,15 @@ function buildDefaultFetcher(content) {
269
269
  * through this transport, bypassing foundation routes and the framework
270
270
  * default. Used only by the editor's preview iframe — normal sites never
271
271
  * pass this option.
272
+ * @param {(target: { locale: string, route: string, url: string }) => void} [options.localeSwitch] -
273
+ * Locale switch override: called with the locale, the page's canonical route and
274
+ * the URL a page load would go to, instead of the page load
275
+ * (`Website#switchLocale`; a click on a link to another locale's page too). For a
276
+ * host that hands the runtime each locale's content itself, like the editor's
277
+ * preview iframe, which a page load would leave.
272
278
  * @returns {import('@uniweb/core').Uniweb}
273
279
  */
274
- export function initUniweb({ content, foundation, extensions = [], routingComponents = DEFAULT_ROUTING_COMPONENTS, transport = null }) {
280
+ export function initUniweb({ content, foundation, extensions = [], routingComponents = DEFAULT_ROUTING_COMPONENTS, transport = null, localeSwitch = null }) {
275
281
  const defaultFetcher = buildDefaultFetcher(content)
276
282
  const dev = !!(import.meta.env && import.meta.env.DEV)
277
283
  const uniweb = createUniweb(content, foundation, extensions, { defaultFetcher, transport, dev })
@@ -308,6 +314,7 @@ export function initUniweb({ content, foundation, extensions = [], routingCompon
308
314
  }
309
315
 
310
316
  uniweb.routingComponents = routingComponents
317
+ uniweb.localeSwitch = localeSwitch
311
318
  // `config.icons`, NOT `content.icons` — the latter is the build's manifest
312
319
  // (`{used, families, …}`), which carries no `cdnUrl`, so passing it here
313
320
  // pinned every browser fetch to the built-in default and made `icons.cdnUrl`