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/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/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/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..68450dde6 --- /dev/null +++ b/src/runtime/plugins/route-resolution-enhancement.ts @@ -0,0 +1,42 @@ +import { defineNuxtPlugin, useNuxtApp } from '#imports' +import { resolveRoute } from '../routing/routing' +import { useRouter } from 'vue-router' +import { isString } from '@intlify/shared' + +import type { Locale } from 'vue-i18n' + +type ResolveParams = Parameters + +export default defineNuxtPlugin({ + name: 'i18n:route-resolution-enhancement', + dependsOn: ['i18n:plugin'], + setup() { + const nuxt = /*#__PURE__*/ useNuxtApp() + if (!__I18N_ROUTE_RESOLUTION__) return + + const ctx = nuxt._nuxtI18n + const router = useRouter() + + /** + * disable enhancement + * - explicit mode without `locale` + * - implicit mode with `locale: false` + */ + const disableEnhancement = (locale?: Locale | boolean) => + (__I18N_ROUTE_RESOLUTION__ !== 'implicit' && locale == null) || locale === false + + const originalResolve = router.resolve.bind(router) + router.resolve = ( + to: ResolveParams[0], + currentLocation: ResolveParams[1], + { locale }: { locale?: Locale | boolean } = {} + ) => { + if (disableEnhancement(locale)) { + return originalResolve(to, currentLocation) + } + + // if `locale` is `false` or `undefined`, use the current locale + return resolveRoute(ctx, to, isString(locale) ? locale : ctx.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