Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: common composable option passing #2737

Merged
merged 8 commits into from
Jan 29, 2024
31 changes: 31 additions & 0 deletions specs/fixtures/issues/2726/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useLocaleHead, useLocalePath, useLocaleRoute, useRouteBaseName, useSwitchLocalePath } from '#i18n'
import { computed, useHead } from '#imports'

const localePath = useLocalePath()
const localeRoute = useLocaleRoute()
const switchLocalePath = useSwitchLocalePath()
const routeBaseName = useRouteBaseName()
const localeHead = useLocaleHead({ addDirAttribute: true, addSeoAttributes: true, identifierAttribute: 'id' })

const metaTestEntries = computed(() => [
{ id: 'locale-path', content: localePath('/nested/test-route') },
{ id: 'locale-route', content: localeRoute('/nested/test-route')?.fullPath ?? '' },
{ id: 'switch-locale-path', content: switchLocalePath('fr') },
{ id: 'route-base-name', content: routeBaseName(localeRoute('/nested/test-route', 'fr')) ?? '' }
])

useHead({
htmlAttrs: {
...localeHead.value.htmlAttrs
},
link: () => [...(localeHead.value.link ?? [])],
meta: () => [...(localeHead.value.meta ?? []), ...metaTestEntries.value]
})
</script>

<template>
<div>
<NuxtPage />
</div>
</template>
19 changes: 19 additions & 0 deletions specs/fixtures/issues/2726/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// https://nuxt.com/docs/guide/directory-structure/nuxt.config
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
debug: false,
i18n: {
defaultLocale: 'en',
strategy: 'prefix_and_default',
locales: [
{
code: 'en',
iso: 'en-US'
},
{
code: 'fr',
iso: 'fr-FR'
}
]
}
})
14 changes: 14 additions & 0 deletions specs/fixtures/issues/2726/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "nuxt3-test-basic-usage-layers",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxi dev",
"build": "nuxt build",
"start": "node .output/server/index.mjs"
},
"devDependencies": {
"@nuxtjs/i18n": "latest",
"nuxt": "latest"
}
}
3 changes: 3 additions & 0 deletions specs/fixtures/issues/2726/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<h1>Home</h1>
</template>
3 changes: 3 additions & 0 deletions specs/fixtures/issues/2726/pages/nested/test-route.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>Test route</div>
</template>
20 changes: 20 additions & 0 deletions specs/issues/2726.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { test, expect, describe } from 'vitest'
import { fileURLToPath } from 'node:url'
import { setup, $fetch } from '../utils'
import { getDom } from '../helper'

describe('#2726 - Composable requires access to the Nuxt instance', async () => {
await setup({
rootDir: fileURLToPath(new URL(`../fixtures/issues/2726`, import.meta.url))
})

test('Composables correctly initialize common options, no internal server error', async () => {
const html = await $fetch('/')
const dom = getDom(html)

expect(dom.querySelector('head #locale-path').content).toEqual('/nested/test-route')
expect(dom.querySelector('head #locale-route').content).toEqual('/nested/test-route')
expect(dom.querySelector('head #switch-locale-path').content).toEqual('/fr')
expect(dom.querySelector('head #route-base-name').content).toEqual('nested-test-route')
})
})
84 changes: 57 additions & 27 deletions src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useRoute, useRouter, useRequestHeaders, useCookie as useNuxtCookie } from '#imports'
import { useRequestHeaders, useCookie as useNuxtCookie } from '#imports'
import { ref, computed, watch, onUnmounted } from 'vue'
import { parseAcceptLanguage } from '../internal'
import { localeCodes, normalizedLocales, nuxtI18nOptions } from '#build/i18n.options.mjs'
import { getActiveHead } from 'unhead'
import { useI18n } from 'vue-i18n'
import { getNormalizedLocales } from '../utils'
import { getNormalizedLocales, initCommonComposableOptions } from '../utils'
import {
getAlternateOgLocales,
getCanonicalLink,
Expand All @@ -18,17 +17,27 @@ import {
localeRoute,
switchLocalePath
} from '../routing/compatibles'
import { findBrowserLocale, getLocale, getLocales } from '../routing/utils'
import { findBrowserLocale, getComposer, getLocale, getLocales } from '../routing/utils'

import type { Ref } from 'vue'
import type { Locale } from 'vue-i18n'
import type { RouteLocation, RouteLocationNormalizedLoaded, RouteLocationRaw, Router } from 'vue-router'
import type { I18nHeadMetaInfo, I18nHeadOptions, SeoAttributesOptions } from '#build/i18n.options.mjs'
import type { HeadParam } from '../utils'
import type { CommonComposableOptions, HeadParam } from '../utils'

export * from 'vue-i18n'
export * from './shared'

type TailParameters<T> = T extends (first: CommonComposableOptions, ...rest: infer R) => unknown ? R : never

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapComposable<F extends (common: CommonComposableOptions, ...args: any[]) => any>(
fn: F,
common = initCommonComposableOptions()
) {
return (...args: TailParameters<F>) => fn(common, ...args)
}

/**
* Returns a function to set i18n params.
*
Expand All @@ -40,28 +49,29 @@ export * from './shared'
*/
export type SetI18nParamsFunction = (params: Record<string, unknown>) => void
export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nParamsFunction {
const route = useRoute()
const common = initCommonComposableOptions()
const head = getActiveHead()
const i18n = getComposer(common.i18n)
const router = common.router

const i18n = useI18n()
const locale = getLocale(i18n)
const locales = getNormalizedLocales(getLocales(i18n))
const _i18nParams = ref({})

const i18nParams = computed({
get() {
return route.meta.nuxtI18n ?? {}
return router.currentRoute.value.meta.nuxtI18n ?? {}
},
set(val) {
_i18nParams.value = val
route.meta.nuxtI18n = val
router.currentRoute.value.meta.nuxtI18n = val
}
})

const stop = watch(
() => route.fullPath,
() => router.currentRoute.value.fullPath,
() => {
route.meta.nuxtI18n = _i18nParams.value
router.currentRoute.value.meta.nuxtI18n = _i18nParams.value
}
)

Expand All @@ -85,12 +95,12 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP

// prettier-ignore
metaObject.link.push(
...getHreflangLinks(locales, idAttribute),
...getCanonicalLink(idAttribute, seoAttributes)
...getHreflangLinks(common, locales, idAttribute),
...getCanonicalLink(common, idAttribute, seoAttributes)
)

metaObject.meta.push(
...getOgUrl(idAttribute, seoAttributes),
...getOgUrl(common, idAttribute, seoAttributes),
...getCurrentOgLocale(currentLocale, currentLocaleIso, idAttribute),
...getAlternateOgLocales(locales, currentLocaleIso, idAttribute)
)
Expand All @@ -105,10 +115,26 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP
}
}

/**
* The `localeHead` function returns localized head properties for locale-related aspects.
*
* @remarks
* The parameter signature of this function is the same as {@link localeHead}.
*
* @param options - An options object, see {@link I18nHeadOptions}
*
* @returns the route object for a given route, the route object is resolved by vue-router rather than just a full route path.
*
* @see {@link localeHead}
*
* @public
*/
export type LocaleHeadFunction = (options: I18nHeadOptions) => ReturnType<typeof localeHead>

/**
* The `useLocaleHead` composable returns localized head properties for locale-related aspects.
*
* @param options - An options, see about details {@link I18nHeadOptions}
* @param options - An options object, see {@link I18nHeadOptions}
*
* @returns The localized {@link I18nHeadMetaInfo | head properties} with Vue `ref`.
*
Expand All @@ -119,8 +145,7 @@ export function useLocaleHead({
addSeoAttributes = false,
identifierAttribute = 'hid'
}: I18nHeadOptions = {}): Ref<I18nHeadMetaInfo> {
const router = useRouter()

const common = initCommonComposableOptions()
const metaObject: Ref<I18nHeadMetaInfo> = ref({
htmlAttrs: {},
link: [],
Expand All @@ -136,12 +161,16 @@ export function useLocaleHead({
}

function updateMeta() {
metaObject.value = localeHead({ addDirAttribute, addSeoAttributes, identifierAttribute }) as I18nHeadMetaInfo
metaObject.value = localeHead(common, {
addDirAttribute,
addSeoAttributes,
identifierAttribute
}) as I18nHeadMetaInfo
}

if (process.client) {
const stop = watch(
() => router.currentRoute.value,
() => common.router.currentRoute.value,
() => {
cleanMeta()
updateMeta()
Expand Down Expand Up @@ -190,7 +219,7 @@ export function useRouteBaseName(): RouteBaseNameFunction {
* The function that resolve locale path.
*
* @remarks
* The parameter sygnatures of this function is same as {@link localePath}.
* The parameter signature of this function is same as {@link localePath}.
*
* @param route - A route location. The path or name of the route or an object for more complex routes.
* @param locale - A locale optional, if not specified, uses the current locale.
Expand All @@ -214,14 +243,14 @@ export type LocalePathFunction = (route: RouteLocation | RouteLocationRaw, local
* @public
*/
export function useLocalePath(): LocalePathFunction {
return localePath
return wrapComposable(localePath)
}

/**
* The function that resolve route.
*
* @remarks
* The parameter sygnatures of this function is same as {@link localeRoute}.
* The parameter signature of this function is same as {@link localeRoute}.
*
* @param route - A route location. The path or name of the route or an object for more complex routes.
* @param locale - A locale optinal, if not specified, uses the current locale.
Expand All @@ -248,14 +277,14 @@ export type LocaleRouteFunction = (
* @public
*/
export function useLocaleRoute(): LocaleRouteFunction {
return localeRoute
return wrapComposable(localeRoute)
}

/**
* The function that resolve locale location.
*
* @remarks
* The parameter sygnatures of this function is same as {@link localeLocation}.
* The parameter signature of this function is same as {@link localeLocation}.
*
* @param route - A route location. The path or name of the route or an object for more complex routes.
* @param locale - A locale optional, if not specified, uses the current locale.
Expand All @@ -279,14 +308,14 @@ export type LocaleLocationFunction = (route: RouteLocationRaw, locale?: Locale)
* @public
*/
export function useLocaleLocation(): LocaleLocationFunction {
return localeLocation
return wrapComposable(localeLocation)
}

/**
* The functin that swtich locale path.
*
* @remarks
* The parameter sygnatures of this function is same as {@link switchLocalePath}.
* The parameter signature of this function is same as {@link switchLocalePath}.
*
* @param locale - A locale optional, if not specified, uses the current locale.
*
Expand All @@ -308,8 +337,9 @@ export type SwitchLocalePathFunction = (locale: Locale) => string
*
* @public
*/

export function useSwitchLocalePath(): SwitchLocalePathFunction {
return switchLocalePath
return wrapComposable(switchLocalePath)
}

/**
Expand Down
23 changes: 1 addition & 22 deletions src/runtime/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ import {
nuxtI18nOptions,
normalizedLocales
} from '#build/i18n.options.mjs'
import { findBrowserLocale, getLocalesRegex, getI18nTarget } from './routing/utils'

import type { NuxtApp } from '#app'
import type { Locale } from 'vue-i18n'
import type { DetectBrowserLanguageOptions, LocaleObject } from '#build/i18n.options.mjs'
import { findBrowserLocale, getLocalesRegex, getI18nTarget } from './routing/utils'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'

export function formatMessage(message: string) {
Expand All @@ -47,26 +46,6 @@ export function defineGetter<K extends string | number | symbol, V>(obj: Record<
Object.defineProperty(obj, key, { get: () => val })
}

export function proxyNuxt<T extends (...args: any) => any>(nuxt: NuxtApp, target: T) {
return function () {
return Reflect.apply(
target,
{
i18n: (nuxt as any).$i18n,
getRouteBaseName: nuxt.$getRouteBaseName,
localePath: nuxt.$localePath,
localeRoute: nuxt.$localeRoute,
switchLocalePath: nuxt.$switchLocalePath,
localeHead: nuxt.$localeHead,
route: (nuxt as any).$router.currentRoute.value,
router: (nuxt as any).$router
},
// eslint-disable-next-line prefer-rest-params
arguments
) as (this: NuxtApp, ...args: Parameters<T>) => ReturnType<T>
}
}

/**
* Parses locales provided from browser through `accept-language` header.
*
Expand Down