From ec95af976630f011eda3044799fb366d5b560aea Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 25 Apr 2025 17:57:59 +0200 Subject: [PATCH 1/3] feat: route resolution enhancement --- src/constants.ts | 3 +- src/gen.ts | 17 +++++++- src/prepare/runtime.ts | 1 + src/runtime/components/NuxtLinkLocale.ts | 7 +-- .../plugins/route-resolution-enhancement.ts | 43 +++++++++++++++++++ src/runtime/routing/routing.ts | 10 ++--- src/types.ts | 10 +++++ 7 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 src/runtime/plugins/route-resolution-enhancement.ts diff --git a/src/constants.ts b/src/constants.ts index 9e43fd12d..c0a915295 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -29,7 +29,8 @@ export const DEFAULT_OPTIONS = { localeDetector: '', typedPages: true, typedOptionsAndMessages: false, - alternateLinkCanonicalQueries: true + alternateLinkCanonicalQueries: true, + routeResolutionEnhancement: false as false | 'explicit' | 'implicit' }, bundle: { compositionOnly: true, diff --git a/src/gen.ts b/src/gen.ts index 5fd2a2be2..4a558ec21 100644 --- a/src/gen.ts +++ b/src/gen.ts @@ -196,7 +196,7 @@ declare global { // prettier-ignore return `// Generated by @nuxtjs/i18n -import type { ${i18nType} } from 'vue-i18n' +import type { ${i18nType}, Locale } from 'vue-i18n' import type { ComposerCustomProperties } from '${relative( join(nuxt.options.buildDir, 'types'), resolve(runtimeDir, 'types.ts') @@ -228,6 +228,21 @@ declare module '#app' { ${typedRouterAugmentations} +declare module 'vue-router' { + interface Router { + resolve( + to: RouteLocationAsRelativeTyped, + currentLocation?: RouteLocationNormalizedLoaded, + options?: { locale?: Locale | boolean } + ): RouteLocationResolved + resolve( + to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath, + currentLocation?: RouteLocationNormalizedLoaded, + options?: { locale?: Locale | boolean } + ): RouteLocationResolved + } +} + ${(options.autoDeclare && globalTranslationTypes) || ''} export {}` diff --git a/src/prepare/runtime.ts b/src/prepare/runtime.ts index 0e12f822f..9cdc0e707 100644 --- a/src/prepare/runtime.ts +++ b/src/prepare/runtime.ts @@ -9,6 +9,7 @@ export function prepareRuntime(ctx: I18nNuxtContext, nuxt: Nuxt) { const { options, resolver } = ctx // for core plugin addPlugin(resolver.resolve('./runtime/plugins/i18n')) + addPlugin(resolver.resolve('./runtime/plugins/route-resolution-enhancement')) addPlugin(resolver.resolve('./runtime/plugins/route-locale-detect')) addPlugin(resolver.resolve('./runtime/plugins/ssg-detect')) addPlugin(resolver.resolve('./runtime/plugins/switch-locale-path-ssr')) diff --git a/src/runtime/components/NuxtLinkLocale.ts b/src/runtime/components/NuxtLinkLocale.ts index 9117e4d8d..c404a503c 100644 --- a/src/runtime/components/NuxtLinkLocale.ts +++ b/src/runtime/components/NuxtLinkLocale.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { isObject } from '@intlify/shared' -import { useLocalePath, type Locale } from '#i18n' +import { useLocaleRoute, type Locale } from '#i18n' import { defineComponent, computed, h } from 'vue' import { defineNuxtLink } from '#imports' import { hasProtocol } from 'ufo' @@ -28,7 +28,7 @@ export default defineComponent({ } }, setup(props, { slots }) { - const localePath = useLocalePath() + const localeRoute = useLocaleRoute() // From https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-link.ts#L57 const checkPropConflicts = ( @@ -43,7 +43,7 @@ export default defineComponent({ const resolvedPath = computed(() => { const destination = props.to ?? props.href - return (destination != null ? localePath(destination, props.locale) : destination) as string + return destination != null ? localeRoute(destination, props.locale) : destination }) // Resolving link type @@ -77,6 +77,7 @@ export default defineComponent({ } if (!isExternal.value) { + // @ts-expect-error type needs to expanded to allow route objects/paths as NuxtLinkProps _props.to = resolvedPath.value } diff --git a/src/runtime/plugins/route-resolution-enhancement.ts b/src/runtime/plugins/route-resolution-enhancement.ts new file mode 100644 index 000000000..5065f3deb --- /dev/null +++ b/src/runtime/plugins/route-resolution-enhancement.ts @@ -0,0 +1,43 @@ +import { defineNuxtPlugin } from '#imports' +import type { Locale } from 'vue-i18n' +import { resolveRoute } from '../routing/routing' +import { useNuxtApp } from 'nuxt/app' +import { useRouter } from 'vue-router' +import type { I18nPublicRuntimeConfig } from '#internal-i18n-types' + +type ResolverParams = Parameters + +export default defineNuxtPlugin({ + name: 'i18n:route-resolution-enhancement', + dependsOn: ['i18n:plugin'], + setup() { + const nuxt = useNuxtApp() + const runtimeI18n = nuxt.$config.public.i18n as I18nPublicRuntimeConfig + + if (!runtimeI18n.experimental.routeResolutionEnhancement) return + + const router = useRouter() + const implicit = runtimeI18n.experimental.routeResolutionEnhancement === 'implicit' + + const originalResolve = router.resolve.bind(router) + router.resolve = ( + to: ResolverParams[0], + currentLocation: ResolverParams[1], + options?: { locale?: Locale | boolean } + ) => { + /** + * disable enhancement + * - explicit mode without `locale` + * - implicit mode with `locale: false` + */ + if ((!implicit && options?.locale == null) || options?.locale === false) { + return originalResolve(to, currentLocation) + } + + // resolve to string | undefined + const _locale = (typeof options?.locale === 'string' && options?.locale) || undefined + console.log(_locale) + return resolveRoute(nuxt._nuxtI18n, to, _locale ?? nuxt._nuxtI18n.getLocale()) + } + } +}) diff --git a/src/runtime/routing/routing.ts b/src/runtime/routing/routing.ts index 71ccaa21b..8d1192d6f 100644 --- a/src/runtime/routing/routing.ts +++ b/src/runtime/routing/routing.ts @@ -54,21 +54,21 @@ function normalizeRawLocation(route: RouteLocationRaw): RouteLike { } /** - * Try resolving route and throw on failure + * Resolve route, throws on failure */ -function resolveRoute(ctx: ComposableContext, route: RouteLocationRaw, locale: Locale) { +export function resolveRoute(ctx: ComposableContext, route: RouteLocationRaw, locale: Locale) { const normalized = normalizeRawLocation(route) - const resolved = ctx.router.resolve(ctx.resolveLocalizedRouteObject(normalized, locale)) + const resolved = ctx.router.resolve(ctx.resolveLocalizedRouteObject(normalized, locale), undefined, { locale: false }) if (resolved.name) { return resolved } // if unable to resolve route try resolving route based on original input - return ctx.router.resolve(route) + return ctx.router.resolve(route, undefined, { locale: false }) } /** - * Try resolving route and return undefined on failure + * Resolve route, returns undefined on failure */ function tryResolveRoute(ctx: ComposableContext, route: RouteLocationRaw, locale: Locale = ctx.getLocale()) { try { diff --git a/src/types.ts b/src/types.ts index 71a61a5be..b5f140dcb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -86,6 +86,16 @@ export interface ExperimentalFeatures { * @default true */ alternateLinkCanonicalQueries?: boolean + + /** + * Enhance Vue Router's route resolution with localization + * + * @defaultValue `false` + * + * @remark `'explicit'` - resolve localized routes when passing `{ locale: Locale | true }` as third argument to router.resolve. + * @remark `'implicit'` - resolve localized routes by default + */ + routeResolutionEnhancement?: false | 'explicit' | 'implicit' } export interface BundleOptions From 783036fbcd39ebecb54467bca905c7f18a59b450 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 25 Apr 2025 18:13:14 +0200 Subject: [PATCH 2/3] refactor: cleanup --- .../plugins/route-resolution-enhancement.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/runtime/plugins/route-resolution-enhancement.ts b/src/runtime/plugins/route-resolution-enhancement.ts index 5065f3deb..ab24fa735 100644 --- a/src/runtime/plugins/route-resolution-enhancement.ts +++ b/src/runtime/plugins/route-resolution-enhancement.ts @@ -1,11 +1,12 @@ -import { defineNuxtPlugin } from '#imports' -import type { Locale } from 'vue-i18n' +import { defineNuxtPlugin, useNuxtApp } from '#imports' import { resolveRoute } from '../routing/routing' -import { useNuxtApp } from 'nuxt/app' import { useRouter } from 'vue-router' +import { isString } from '@intlify/shared' + +import type { Locale } from 'vue-i18n' import type { I18nPublicRuntimeConfig } from '#internal-i18n-types' -type ResolverParams = Parameters +type ResolveParams = Parameters export default defineNuxtPlugin({ name: 'i18n:route-resolution-enhancement', @@ -13,31 +14,31 @@ export default defineNuxtPlugin({ setup() { const nuxt = useNuxtApp() const runtimeI18n = nuxt.$config.public.i18n as I18nPublicRuntimeConfig - if (!runtimeI18n.experimental.routeResolutionEnhancement) return + const ctx = nuxt._nuxtI18n const router = useRouter() const implicit = runtimeI18n.experimental.routeResolutionEnhancement === 'implicit' + /** + * disable enhancement + * - explicit mode without `locale` + * - implicit mode with `locale: false` + */ + const disableEnhancement = (locale?: Locale | boolean) => (!implicit && locale == null) || locale === false + const originalResolve = router.resolve.bind(router) router.resolve = ( - to: ResolverParams[0], - currentLocation: ResolverParams[1], - options?: { locale?: Locale | boolean } + to: ResolveParams[0], + currentLocation: ResolveParams[1], + { locale }: { locale?: Locale | boolean } = {} ) => { - /** - * disable enhancement - * - explicit mode without `locale` - * - implicit mode with `locale: false` - */ - if ((!implicit && options?.locale == null) || options?.locale === false) { + if (disableEnhancement(locale)) { return originalResolve(to, currentLocation) } - // resolve to string | undefined - const _locale = (typeof options?.locale === 'string' && options?.locale) || undefined - console.log(_locale) - return resolveRoute(nuxt._nuxtI18n, to, _locale ?? nuxt._nuxtI18n.getLocale()) + // if `locale` is `false` or `undefined`, use the current locale + return resolveRoute(ctx, to, isString(locale) ? locale : ctx.getLocale()) } } }) From c792a3dd58774825322e5003cd87c38607430c94 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 25 Apr 2025 18:26:32 +0200 Subject: [PATCH 3/3] perf: use compiler constant for tree-shaking --- src/bundler.ts | 3 ++- src/env.d.ts | 1 + src/runtime/plugins/route-resolution-enhancement.ts | 10 ++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bundler.ts b/src/bundler.ts index 7663b751c..bad7e2031 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -131,7 +131,8 @@ export function getDefineConfig(options: NuxtI18nOptions, server = false, nuxt = __ROUTE_NAME_SEPARATOR__: JSON.stringify(options.routesNameSeparator), __ROUTE_NAME_DEFAULT_SUFFIX__: JSON.stringify(options.defaultLocaleRouteNameSuffix), __TRAILING_SLASH__: String(options.trailingSlash), - __DEFAULT_DIRECTION__: JSON.stringify(options.defaultDirection) + __DEFAULT_DIRECTION__: JSON.stringify(options.defaultDirection), + __I18N_ROUTE_RESOLUTION__: JSON.stringify(options.experimental?.routeResolutionEnhancement ?? false) } if (nuxt.options.ssr || !server) { diff --git a/src/env.d.ts b/src/env.d.ts index 689b26efa..aa9b52deb 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -23,3 +23,4 @@ declare let __I18N_STRATEGY__: 'no_prefix' | 'prefix' | 'prefix_except_default' declare let __ROUTE_NAME_SEPARATOR__: string declare let __ROUTE_NAME_DEFAULT_SUFFIX__: string declare let __DEFAULT_DIRECTION__: string +declare let __I18N_ROUTE_RESOLUTION__: boolean | 'implicit' | 'explicit' diff --git a/src/runtime/plugins/route-resolution-enhancement.ts b/src/runtime/plugins/route-resolution-enhancement.ts index ab24fa735..68450dde6 100644 --- a/src/runtime/plugins/route-resolution-enhancement.ts +++ b/src/runtime/plugins/route-resolution-enhancement.ts @@ -4,7 +4,6 @@ import { useRouter } from 'vue-router' import { isString } from '@intlify/shared' import type { Locale } from 'vue-i18n' -import type { I18nPublicRuntimeConfig } from '#internal-i18n-types' type ResolveParams = Parameters @@ -12,20 +11,19 @@ export default defineNuxtPlugin({ name: 'i18n:route-resolution-enhancement', dependsOn: ['i18n:plugin'], setup() { - const nuxt = useNuxtApp() - const runtimeI18n = nuxt.$config.public.i18n as I18nPublicRuntimeConfig - if (!runtimeI18n.experimental.routeResolutionEnhancement) return + const nuxt = /*#__PURE__*/ useNuxtApp() + if (!__I18N_ROUTE_RESOLUTION__) return const ctx = nuxt._nuxtI18n const router = useRouter() - const implicit = runtimeI18n.experimental.routeResolutionEnhancement === 'implicit' /** * disable enhancement * - explicit mode without `locale` * - implicit mode with `locale: false` */ - const disableEnhancement = (locale?: Locale | boolean) => (!implicit && locale == null) || locale === false + const disableEnhancement = (locale?: Locale | boolean) => + (__I18N_ROUTE_RESOLUTION__ !== 'implicit' && locale == null) || locale === false const originalResolve = router.resolve.bind(router) router.resolve = (