Skip to content

feat(Link): auto-localize internal links when @nuxtjs/i18n is installed#5537

Merged
benjamincanac merged 10 commits intov4from
feat/5536
Apr 17, 2026
Merged

feat(Link): auto-localize internal links when @nuxtjs/i18n is installed#5537
benjamincanac merged 10 commits intov4from
feat/5536

Conversation

@benjamincanac
Copy link
Copy Markdown
Member

@benjamincanac benjamincanac commented Nov 26, 2025

🔗 Linked issue

Resolves #5536, resolves #5359, resolves #5637, resolves #4465

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@github-actions github-actions Bot added the v4 #4488 label Nov 26, 2025
@benjamincanac
Copy link
Copy Markdown
Member Author

@BobbieGoede I'd love your feedback on this, would this work?

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Nov 26, 2025

npm i https://pkg.pr.new/@nuxt/ui@5537

commit: 4ca5bc7

Comment thread src/runtime/components/Link.vue Outdated
benjamincanac and others added 2 commits December 8, 2025 14:37
Co-authored-by: Bobbie Goede <bobbiegoede@gmail.com>
Comment thread src/runtime/components/Link.vue Outdated
@benjamincanac
Copy link
Copy Markdown
Member Author

@BobbieGoede I have another question, what will happen if users have already passed localePath or localeRoute to the to prop? I don't want to break existing apps 😬

@BobbieGoede
Copy link
Copy Markdown
Member

@BobbieGoede I have another question, what will happen if users have already passed localePath or localeRoute to the to prop? I don't want to break existing apps 😬

Passing localized paths/routes to localePath and localeRoute should work, but will still localize it to the current language since no locale parameter is passed, so this is breaking for users passing localized paths pointing to other languages.

You might want to add a prop to the component so users can both opt-in to localization and/or specify the language (something like locale: true | string). You could also add a config option to invert it so that it's opt-out instead (like this draft for example nuxt-modules/i18n#3530).

@chenggoi
Copy link
Copy Markdown

watch

1 similar comment
@supersonictw
Copy link
Copy Markdown

watch

@g0shed
Copy link
Copy Markdown

g0shed commented Jan 21, 2026

any advance on this? or even any workaround?

@benjamincanac benjamincanac changed the title feat(Link): automatically use localePath from @nuxtjs/i18n feat(Link): auto-localize internal links when @nuxtjs/i18n is installed Apr 17, 2026
@benjamincanac benjamincanac marked this pull request as ready for review April 17, 2026 15:27
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Adds automatic i18n localization to the Link component: when @nuxtjs/i18n is present the component retrieves nuxtApp.$localePath via useNuxtApp() and rewrites internal to paths with it. The to computation now returns early for falsy values, leaves non-string RouteLocationRaw values and external or protocol-containing URLs unchanged (including when props.external is true or hasProtocol detects a protocol), and falls back to the original path when $localePath is unavailable. Documentation pages were updated to describe this behavior and include examples; manual localePath()/localeRoute() usage remains supported.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding automatic localization of internal links in the Link component when @nuxtjs/i18n is installed, which aligns perfectly with the code changes.
Description check ✅ Passed The PR description links to four relevant issues (#5536, #5359, #5637, #4465) related to i18n integration and properly categorizes the change as an enhancement.
Linked Issues check ✅ Passed The implementation adds automatic locale path rewriting to the Link component via $localePath, addressing the core requirement from #5536 to support locale-aware routing without manual useLocalePath calls, and supporting #5359, #5637, and #4465's broader i18n integration goals.
Out of Scope Changes check ✅ Passed All changes are in scope: Link.vue component implementation, plus documentation updates for nuxt.md and link.md directly related to the new auto-localization feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/5536

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/2.components/link.md`:
- Around line 102-104: The docs overstate idempotency — the Link component
implementation (Link.vue) simply forwards string paths to $localePath so whether
already-localized paths are preserved depends on the i18n strategy; update the
wording to a conditional claim like "In most configurations the Link component
preserves already-localized paths; if you encounter edge cases use localePath()
manually" and add a short note referencing the Link component's behavior (the
string path forwarding to $localePath) so readers know why strategy differences
matter.

In `@src/runtime/components/Link.vue`:
- Around line 149-165: The computed to() currently calls nuxtApp.$localePath for
any non-external path which can double-localize already-prefixed strings and
will coerce route objects to strings (dropping state/params), breaking
downstream checks like isInternalLink; fix by only invoking nuxtApp.$localePath
when path is a plain non-prefixed string (e.g., typeof path === 'string' and
does not already include a locale prefix), leave route objects untouched (do not
call $localePath on objects) and keep external/absolute checks as-is so
already-localized or object routes are returned unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d9449a8-6338-4e47-9736-3c9f4f80e2fe

📥 Commits

Reviewing files that changed from the base of the PR and between 6aba2ea and 27b9ced.

📒 Files selected for processing (3)
  • docs/content/docs/1.getting-started/6.integrations/4.i18n/1.nuxt.md
  • docs/content/docs/2.components/link.md
  • src/runtime/components/Link.vue

Comment thread docs/content/docs/2.components/link.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/content/docs/2.components/link.md (1)

93-108: Document the external/absolute URL exception explicitly.

The runtime logic skips localization for external/absolute URLs and also allows forcing this via external; adding one sentence here would better match behavior and reduce ambiguity.

✏️ Suggested doc tweak
 The Link component automatically integrates with [`@nuxtjs/i18n`](https://i18n.nuxtjs.org/) when installed. Internal links are automatically localized using the `$localePath` helper without requiring manual wrapping.
+
+External or absolute URLs (including protocol-based links) are not localized, and you can use the `external` prop to force bypass localization for edge cases.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/2.components/link.md` around lines 93 - 108, Add a
one-sentence clarification to the Link docs explaining that the Link/ULink
component will not localize external or absolute URLs (it skips calling
$localePath) and that this behavior can also be forced by setting the external
prop; mention that developers can still manually call localePath() or
localeRoute() for custom behavior. Reference the Link/ULink component and the
helpers ($localePath, localePath(), localeRoute()) so readers know where the
exception applies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/content/docs/2.components/link.md`:
- Around line 93-108: Add a one-sentence clarification to the Link docs
explaining that the Link/ULink component will not localize external or absolute
URLs (it skips calling $localePath) and that this behavior can also be forced by
setting the external prop; mention that developers can still manually call
localePath() or localeRoute() for custom behavior. Reference the Link/ULink
component and the helpers ($localePath, localePath(), localeRoute()) so readers
know where the exception applies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0d13a2c-296b-4976-aa9b-975592759076

📥 Commits

Reviewing files that changed from the base of the PR and between 27b9ced and 6503cde.

📒 Files selected for processing (1)
  • docs/content/docs/2.components/link.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/runtime/components/Link.vue (1)

162-165: Prefer $localeRoute over $localePath to better preserve route semantics.

$localeRoute returns a resolved route object preserving query, params, state, and other route properties, whereas $localePath collapses everything to a string. Since RouterLinkProps['to'] expects RouteLocationRaw and the isInternalLink computed already handles non-string route objects (line 174), switching to $localeRoute is safe and improves route data preservation downstream.

♻️ Proposed change
-  // Use `$localePath` from `@nuxtjs/i18n` if available
-  const localePath = nuxtApp.$localePath as ((route: RouteLocationRaw, locale?: string) => string) | undefined
-  if (localePath) {
-    return localePath(path)
+  // Use `$localeRoute` from `@nuxtjs/i18n` if available (preserves route properties like `state`)
+  const localeRoute = nuxtApp.$localeRoute as ((route: RouteLocationRaw, locale?: string) => RouteLocationRaw | undefined) | undefined
+  if (localeRoute) {
+    return localeRoute(path) ?? path
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Link.vue` around lines 162 - 165, Replace the use of
nuxtApp.$localePath with nuxtApp.$localeRoute so you return a RouteLocationRaw
instead of a string: create a const (e.g. localeRoute) typed as ((route:
RouteLocationRaw, locale?: string) => RouteLocationRaw) | undefined, call
localeRoute(path) when present and return that object (fall back to the original
path when undefined); this preserves params/query/state for downstream consumers
and matches RouterLinkProps['to'] and the existing isInternalLink handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/components/Link.vue`:
- Around line 149-168: Add a new prop locale?: boolean | string to the Link
component and modify the computed to to pass the explicit locale to
nuxtApp.$localePath when available: if locale === false skip calling $localePath
and return the raw path (to opt-out/detect already-localized paths), if locale
is a string call localePath(path, locale) to target a specific locale, and if
locale is undefined follow a global app.config default (e.g.,
app.config.globalProperties.linkLocaleBehavior or leave current behavior) so
existing use of $localePath remains unchanged; update references in the computed
to check props.locale before invoking nuxtApp.$localePath and keep the typeof
path !== 'string' guard and hasProtocol(props, ...) logic intact.

---

Nitpick comments:
In `@src/runtime/components/Link.vue`:
- Around line 162-165: Replace the use of nuxtApp.$localePath with
nuxtApp.$localeRoute so you return a RouteLocationRaw instead of a string:
create a const (e.g. localeRoute) typed as ((route: RouteLocationRaw, locale?:
string) => RouteLocationRaw) | undefined, call localeRoute(path) when present
and return that object (fall back to the original path when undefined); this
preserves params/query/state for downstream consumers and matches
RouterLinkProps['to'] and the existing isInternalLink handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c753b75d-3965-41fb-a1d8-9bcf6219acb7

📥 Commits

Reviewing files that changed from the base of the PR and between 6503cde and 4ca5bc7.

📒 Files selected for processing (1)
  • src/runtime/components/Link.vue

Comment on lines +149 to +168
const to = computed(() => {
const path = props.to ?? props.href
if (!path) return path

// Only localize string paths, leave route objects untouched to preserve state/params
if (typeof path !== 'string') return path

// Skip external links and absolute URLs
if (props.external || hasProtocol(path, { acceptRelative: true })) {
return path
}

// Use `$localePath` from `@nuxtjs/i18n` if available
const localePath = nuxtApp.$localePath as ((route: RouteLocationRaw, locale?: string) => string) | undefined
if (localePath) {
return localePath(path)
}

return path
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@nuxtjs/i18n $localePath accept a locale argument signature and behavior

💡 Result:

Yes, $localePath in @nuxtjs/i18n accepts a locale argument as the second parameter. It is the global function provided by the module (also available via the useLocalePath composable), used to resolve a localized path for a given route. Signature (from TypeScript definition): (localePath function): (route: RawLocation | RouteLocation, locale?: Locale) => string Parameters: - route (first argument, required): Route name (string), path (string), or route object (e.g., { name: 'index' } or { name: 'category-slug', params: { slug: 'value' } }) - locale (second argument, optional): Locale code string (e.g., 'en', 'fr'). Defaults to the current locale if omitted. Behavior: - Returns the path localized according to the specified locale (or current locale). - Prefixes the path with the locale code based on the routing strategy (e.g., '/en/index' for prefix strategy). - Supports route params for dynamic routes. - Use in templates: <NuxtLink :to="$localePath('index', 'en')"> - In <script setup>: const localePath = useLocalePath; localePath('index', 'en') Examples: - $localePath('index') → '/en' (current locale 'en') - $localePath('index', 'fr') → '/fr' - $localePath({ name: 'products-id', params: { id: 123 } }, 'de') → '/de/produkte/123' (if customized)

Citations:


Add locale prop to allow opt-out and explicit locale targeting before shipping.

The typeof guard at line 154 correctly preserves route objects' state/params. However, the component still lacks control over locale selection. $localePath accepts a locale parameter (e.g., localePath(path, 'fr')), but the current code never uses it, always falling back to the current locale.

Two gaps remain:

  1. No opt-out mechanism: Users intentionally linking to another locale (e.g., /fr/about from an en locale) will have the link silently re-localized, breaking existing use cases. A locale?: boolean | string prop (where locale: false opts out and locale: 'fr' targets a specific locale) would preserve current behavior while enabling cross-locale links.

  2. Double-localization risk: Pre-localized paths like /fr/about passed to an already-localized $localePath across different i18n strategies (prefix, prefix_except_default, etc.) can resolve unpredictably. A locale: false opt-out provides users an escape hatch.

Consider a global app.config toggle (opt-in vs. opt-out behavior by default) alongside the prop to mitigate backward-compat risk for apps already using useLocalePath() output in :to directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Link.vue` around lines 149 - 168, Add a new prop
locale?: boolean | string to the Link component and modify the computed to to
pass the explicit locale to nuxtApp.$localePath when available: if locale ===
false skip calling $localePath and return the raw path (to opt-out/detect
already-localized paths), if locale is a string call localePath(path, locale) to
target a specific locale, and if locale is undefined follow a global app.config
default (e.g., app.config.globalProperties.linkLocaleBehavior or leave current
behavior) so existing use of $localePath remains unchanged; update references in
the computed to check props.locale before invoking nuxtApp.$localePath and keep
the typeof path !== 'string' guard and hasProtocol(props, ...) logic intact.

@benjamincanac benjamincanac merged commit 92cfda0 into v4 Apr 17, 2026
22 checks passed
@benjamincanac benjamincanac deleted the feat/5536 branch April 17, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

5 participants