From 85be9466955d1d47d480d30cf362fb44ed156d92 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Sat, 27 Jun 2026 07:44:09 +0000 Subject: [PATCH] fix(Link): fall back to original path when localePath fails (#6637) Port of upstream nuxt/ui 906e8fd4. When the i18n localePath() returns a falsy value, Link rendered an empty to; fall back to the original path. - Link.vue: const localizedPath = localePath(path, ...); return localizedPath || path. - test/components/nuxt/LinkLocale.spec.ts (new): mocks localePath to '' for Link and Button. Ledger: record merge sha for #222 (0faeb926) and advance cursor to 906e8fd4. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Qz7EXMncvEGiCj4WbmYgJo --- ...06e8fd49f2dbf664133c79f2ebe5569e63d627e.md | 21 +++++++++ .sync/nuxt-ui.json | 12 ++++-- src/runtime/components/Link.vue | 4 +- test/components/nuxt/LinkLocale.spec.ts | 43 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 .sync/log/906e8fd49f2dbf664133c79f2ebe5569e63d627e.md create mode 100644 test/components/nuxt/LinkLocale.spec.ts diff --git a/.sync/log/906e8fd49f2dbf664133c79f2ebe5569e63d627e.md b/.sync/log/906e8fd49f2dbf664133c79f2ebe5569e63d627e.md new file mode 100644 index 000000000..6230557b9 --- /dev/null +++ b/.sync/log/906e8fd49f2dbf664133c79f2ebe5569e63d627e.md @@ -0,0 +1,21 @@ +# Port: fix(Link): fall back to original path when `localePath` fails (#6637) + +**Upstream:** `906e8fd49f2dbf664133c79f2ebe5569e63d627e` (nuxt/ui) +**Decision:** port + +## Upstream change +`Link.vue`: when the i18n `localePath()` returns a falsy value (route can't be +localized), the link rendered an empty `to`. Fall back to the original `path`: +`return localePath(...) || path`. +new nuxt test `LinkLocale.spec.ts`. + +## b24ui adaptation +Direct 1:1 — b24ui's `Link.vue` matched the pre-change. +- `src/runtime/components/Link.vue`: `const localizedPath = localePath(path, + …); return localizedPath || path`. +- `test/components/nuxt/LinkLocale.spec.ts` (new; new `test/components/nuxt/` + dir): ported verbatim — mocks `$localePath` to `() => ''` and asserts both + `Link` and `Button` (with `to`) fall back to `/dashboard`. + +## Verification (pnpm 11.8.0, gate ON) +dev:prepare · lint · typecheck · test (**5141 passed**, 6 skipped) · module +build — all green. No snapshot churn. diff --git a/.sync/nuxt-ui.json b/.sync/nuxt-ui.json index 42580bf28..ac6787541 100644 --- a/.sync/nuxt-ui.json +++ b/.sync/nuxt-ui.json @@ -2,7 +2,7 @@ "upstream": "nuxt/ui", "branch": "v4", "sync_enabled": false, - "cursor": "0faeb9265e720b43021b020c3bd9d7878b9464da", + "cursor": "906e8fd49f2dbf664133c79f2ebe5569e63d627e", "_cursor_note": "cursor = last upstream commit ported into b24ui (oldest-first, manual cadence). sync_enabled stays false until Phase 2 (porter workflow #67 + CLAUDE_CODE_OAUTH_TOKEN) is wired and trusted. `processed` is maintained per port from now on (backfilled #68-#72 on 2026-06-09).", "stats": { "queue_depth": 0, @@ -755,10 +755,16 @@ "summary": "feat(InputRating): new component (#5757) — SKIPPED (maintainer call): brand-new InputRating component, 17 files/+1979 lines (InputRating.vue, theme/input-rating.ts, registration in theme/index.ts + icons.ts + types/index.ts + useComponentProps.ts, tests+snapshot, docs/playground/screenshots). Not a 1:1 port: like Calendar (#194) the theme is built on nuxt/ui theme.colors loops + semantic tokens, while b24ui is air-design-token based with no theme.colors — needs a deliberate air-design pass + full component registration, not mechanical translation. Tracked in issue #220" }, "0faeb9265e720b43021b020c3bd9d7878b9464da": { - "pr": null, - "b24ui_sha": "pending-merge", + "pr": 222, + "b24ui_sha": "3751f71d", "decision": "no-op", "summary": "fix(AuthForm): track password visibility per field (#6638) — NO-OP: upstream edits src/runtime/components/AuthForm.vue (+spec), which b24ui does not have (no AuthForm component anywhere, git ls-files grep authform => none). Nothing to port. Bookkeeping only" + }, + "906e8fd49f2dbf664133c79f2ebe5569e63d627e": { + "pr": null, + "b24ui_sha": "pending-merge", + "decision": "port", + "summary": "fix(Link): fall back to original path when localePath fails (#6637) — Link.vue: return localePath(path,...) -> const localizedPath=localePath(...); return localizedPath || path (empty localePath no longer yields empty to). Direct 1:1 (b24ui matched pre-change). +new test/components/nuxt/LinkLocale.spec.ts (new dir) mocking $localePath=()=>'' for Link+Button. No snapshot churn" } } } diff --git a/src/runtime/components/Link.vue b/src/runtime/components/Link.vue index 620908424..307e95826 100644 --- a/src/runtime/components/Link.vue +++ b/src/runtime/components/Link.vue @@ -184,7 +184,9 @@ const to = computed(() => { return path } - return localePath(path, typeof props.locale === 'string' ? props.locale : undefined) + const localizedPath = localePath(path, typeof props.locale === 'string' ? props.locale : undefined) + + return localizedPath || path }) const isInternalLink = computed(() => { diff --git a/test/components/nuxt/LinkLocale.spec.ts b/test/components/nuxt/LinkLocale.spec.ts new file mode 100644 index 000000000..eaf1b2222 --- /dev/null +++ b/test/components/nuxt/LinkLocale.spec.ts @@ -0,0 +1,43 @@ +import { h, onUnmounted } from 'vue' +import { describe, expect, it } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { useNuxtApp } from '#imports' +import Link from '../../../src/runtime/components/Link.vue' +import Button from '../../../src/runtime/components/Button.vue' + +function createLocalePathFallbackFixture(component: typeof Link | typeof Button) { + return { + setup() { + const nuxtApp = useNuxtApp() as ReturnType + const originalLocalePath = nuxtApp.$localePath + + nuxtApp.$localePath = () => '' + + onUnmounted(() => { + nuxtApp.$localePath = originalLocalePath + }) + + return () => h(component, { to: '/dashboard' }, () => 'Dashboard') + } + } +} + +describe('Link locale fallback', () => { + it('falls back to the original internal path when localePath returns an empty string', async () => { + const wrapper = await mountSuspended(createLocalePathFallbackFixture(Link)) + + const link = wrapper.get('a') + + expect(link.attributes('href')).toBe('/dashboard') + expect(link.text()).toContain('Dashboard') + }) + + it('keeps button links navigable when localePath cannot resolve the route', async () => { + const wrapper = await mountSuspended(createLocalePathFallbackFixture(Button)) + + const link = wrapper.get('a') + + expect(link.attributes('href')).toBe('/dashboard') + expect(link.text()).toContain('Dashboard') + }) +})