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

feat: locales generated type narrowing #2722

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions playground/pages/[...catch].vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script setup lang="ts">
import type { LocaleObject } from '#i18n'

const route = useRoute()
const { locale, locales } = useI18n()

const availableLocales = computed(() => {
return (locales.value as LocaleObject[]).filter(i => i.code !== locale.value)
return locales.value.filter(i => i.code !== locale.value)
})

definePageMeta({
Expand Down
6 changes: 2 additions & 4 deletions playground/pages/about/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { defineComponent } from 'vue'
import type { ExportedGlobalComposer, LocaleObject } from '#i18n'

export default defineComponent({
mounted() {
Expand All @@ -12,11 +11,10 @@ export default defineComponent({
},
computed: {
availableLocales() {
return (this.$i18n.locales as LocaleObject[]).filter(i => i.code !== this.$i18n.locale)
return this.$i18n.locales.filter(i => i.code !== this.$i18n.locale)
},
switchableLocale() {
const i18n = this.$i18n as ExportedGlobalComposer
const _locales = (i18n.locales as LocaleObject[]).filter(i => i.code !== this.$i18n.locale)
const _locales = this.$i18n.locales.filter(i => i.code !== this.$i18n.locale)
return _locales.length !== 0 ? _locales[0] : { code: 'ja', name: 'ζ—₯本θͺž' }
}
},
Expand Down
4 changes: 1 addition & 3 deletions playground/pages/category/[id].vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<script setup lang="ts">
import type { LocaleObject } from '#i18n'

const route = useRoute()
const { locale, locales } = useI18n()

const availableLocales = computed(() => {
return (locales.value as LocaleObject[]).filter(i => i.code !== locale.value)
return locales.value.filter(i => i.code !== locale.value)
})

definePageMeta({
Expand Down
7 changes: 2 additions & 5 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'

// import { useLocalePath, useSwitchLocalePath, useLocaleHead, useBrowserLocale } from '#i18n'
import type { LocaleObject } from '#i18n'

const route = useRoute()
const {
t,
Expand Down Expand Up @@ -34,12 +31,12 @@ console.log('message if github layer merged:', t('layer-test-key'))
console.log('experimental module', t('goodDay'))

function getLocaleName(code: string) {
const locale = (locales.value as LocaleObject[]).find(i => i.code === code)
const locale = locales.value.find(i => i.code === code)
return locale ? locale.name : code
}

const availableLocales = computed(() => {
return (locales.value as LocaleObject[]).filter(i => i.code !== locale.value)
return locales.value.filter(i => i.code !== locale.value)
})

const i = tm('items')
Expand Down
39 changes: 39 additions & 0 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import createDebug from 'debug'
import { EXECUTABLE_EXTENSIONS } from './constants'
import { genImport, genDynamicImport } from 'knitwork'
import { withQuery } from 'ufo'
import { resolve } from 'pathe'
import { distDir, runtimeDir } from './dirs'
import { getLayerI18n, getLocalePaths, toCode } from './utils'

import type { Nuxt } from '@nuxt/schema'
Expand Down Expand Up @@ -151,4 +153,41 @@ function genImportSpecifier(
return getLoadPath()
}

export function generateI18nPageTypes() {
return `// Generated by @nuxtjs/i18n
declare module 'nuxt/dist/pages/runtime' {
interface PageMeta {
nuxtI18n?: Record<string, any>
}
}

export {}`
}

export function generateI18nTypes(nuxt: Nuxt, options: NuxtI18nOptions) {
const vueI18nTypes = options.types === 'legacy' ? ['VueI18n'] : ['ExportedGlobalComposer', 'Composer']
const generatedLocales = simplifyLocaleOptions(nuxt, options)
const resolvedLocaleType = typeof generatedLocales === 'string' ? 'string[]' : 'LocaleObject[]'

// prettier-ignore
return `// Generated by @nuxtjs/i18n
import type { ${vueI18nTypes.join(', ')} } from 'vue-i18n'
import type { NuxtI18nRoutingCustomProperties, ComposerCustomProperties } from '${resolve(runtimeDir, 'types.ts')}'
import type { Strategies, Directions, LocaleObject } from '${resolve(distDir, 'types.d.ts')}'

declare module 'vue-i18n' {
interface ComposerCustom extends ComposerCustomProperties<${resolvedLocaleType}> {}
interface ExportedGlobalComposer extends NuxtI18nRoutingCustomProperties<${resolvedLocaleType}> {}
interface VueI18n extends NuxtI18nRoutingCustomProperties<${resolvedLocaleType}> {}
}

declare module '#app' {
interface NuxtApp {
$i18n: ${vueI18nTypes.join(' & ')} & NuxtI18nRoutingCustomProperties<${resolvedLocaleType}>
}
}

export {}`
}

/* eslint-enable @typescript-eslint/no-explicit-any */
16 changes: 16 additions & 0 deletions src/internal-global-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Composer, ExportedGlobalComposer, VueI18n } from 'vue-i18n'
import type { ComposerCustomProperties, NuxtI18nRoutingCustomProperties } from './runtime/types'

declare module 'vue-i18n' {
interface ComposerCustom extends ComposerCustomProperties {}
interface ExportedGlobalComposer extends NuxtI18nRoutingCustomProperties {}
interface VueI18n extends NuxtI18nRoutingCustomProperties {}
}

declare module '#app' {
interface NuxtApp {
$i18n: VueI18n & ExportedGlobalComposer & Composer & NuxtI18nRoutingCustomProperties & I18nRoutingCustomProperties
}
}

export {}
33 changes: 15 additions & 18 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
addComponent,
addPlugin,
addTemplate,
addTypeTemplate,
addImports,
useLogger
} from '@nuxt/kit'
Expand All @@ -16,7 +17,7 @@ import { setupAlias } from './alias'
import { setupPages } from './pages'
import { setupNitro } from './nitro'
import { extendBundler } from './bundler'
import { generateLoaderOptions } from './gen'
import { generateI18nPageTypes, generateI18nTypes, generateLoaderOptions } from './gen'
import {
NUXT_I18N_MODULE_ID,
DEFAULT_OPTIONS,
Expand All @@ -40,7 +41,7 @@ import { applyLayerOptions, checkLayerOptions, resolveLayerVueI18nConfigInfo } f
import { generateTemplateNuxtI18nOptions } from './template'

import type { HookResult } from '@nuxt/schema'
import type { NuxtI18nOptions, SimpleLocaleObject } from './types'
import type { NuxtI18nOptions } from './types'

export * from './types'

Expand Down Expand Up @@ -213,10 +214,8 @@ export default defineNuxtModule<NuxtI18nOptions>({
nuxtI18nOptions,
isServer
}),
NUXT_I18N_MODULE_ID,
localeCodes,
nuxtI18nOptionsDefault: DEFAULT_OPTIONS,
nuxtI18nInternalOptions: { __normalizedLocales: normalizedLocales as SimpleLocaleObject[] },
normalizedLocales,
dev: nuxt.options.dev,
isSSG: nuxt.options._generate,
parallelPlugin: options.parallelPlugin
Expand All @@ -230,25 +229,23 @@ export default defineNuxtModule<NuxtI18nOptions>({
})

/**
* To be plugged for `PageMeta` type definition on `NuxtApp`
* `PageMeta` augmentation to add `nuxtI18n` property
* TODO: Remove in v8.1, `useSetI18nParams` should be used instead
*/

if (!!options.dynamicRouteParams) {
addPlugin(resolve(runtimeDir, 'plugins/meta'))
addTypeTemplate({
filename: 'types/i18n-page-meta.d.ts',
getContents: () => generateI18nPageTypes()
})
}

/**
* add extend type definition
* `$i18n` type narrowing based on 'legacy' or 'composition'
* `locales` type narrowing based on generated configuration
*/

const isLegacyMode = () => options.types === 'legacy'

// To be plugged for `$i18n` type definition on `NuxtApp`
addPlugin(resolve(runtimeDir, isLegacyMode() ? 'plugins/legacy' : 'plugins/composition'))

nuxt.hook('prepare:types', ({ references }) => {
const vueI18nTypeFilename = resolve(runtimeDir, 'types')
references.push({ path: resolve(nuxt.options.buildDir, vueI18nTypeFilename) })
addTypeTemplate({
filename: 'types/i18n-plugin.d.ts',
getContents: () => generateI18nTypes(nuxt, i18nOptions)
})

/**
Expand Down
5 changes: 2 additions & 3 deletions src/options.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { NuxtI18nOptions, NuxtI18nInternalOptions, VueI18nConfig } from './types'
import type { NuxtI18nOptions, VueI18nConfig } from './types'
import type { NuxtI18nOptionsDefault } from './constants'
import type { DeepRequired } from 'ts-essentials'

Expand All @@ -25,7 +25,7 @@ export const vueI18nConfigs: VueI18nConfig[]
export const localeCodes: string[] = []
export const nuxtI18nOptions: DeepRequired<NuxtI18nOptions<Context>> = {}
export const nuxtI18nOptionsDefault: NuxtI18nOptionsDefault = {}
export const nuxtI18nInternalOptions: DeepRequired<NuxtI18nInternalOptions> = {}
export const normalizedLocales: LocaleObject[] = []
export const NUXT_I18N_MODULE_ID = ''
export const isSSG = false
export const parallelPlugin: boolean
Expand All @@ -43,7 +43,6 @@ export const DEFAULT_DYNAMIC_PARAMS_KEY: typeof constants.DEFAULT_DYNAMIC_PARAMS
export {
NuxtI18nOptions,
NuxtI18nOptionsDefault,
NuxtI18nInternalOptions,
DetectBrowserLanguageOptions,
RootRedirectOptions
} from './types'
48 changes: 18 additions & 30 deletions src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { useRoute, useRequestHeaders, useCookie, useRouter } from '#imports'
import { useRoute, useRouter, useRequestHeaders, useCookie as useNuxtCookie } from '#imports'
import { ref, computed, watch, onUnmounted } from 'vue'
import { parseAcceptLanguage } from '../internal'
import { nuxtI18nInternalOptions, nuxtI18nOptionsDefault, localeCodes as _localeCodes } from '#build/i18n.options.mjs'
import { localeCodes, normalizedLocales, nuxtI18nOptions } from '#build/i18n.options.mjs'
import { getActiveHead } from 'unhead'
import { useI18n } from 'vue-i18n'

import type { Ref } from 'vue'
import type {
DetectBrowserLanguageOptions,
I18nHeadMetaInfo,
I18nHeadOptions,
SeoAttributesOptions
} from '#build/i18n.options.mjs'

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

import { getNormalizedLocales, type HeadParam } from '../utils'
import { getNormalizedLocales } from '../utils'
import {
getAlternateOgLocales,
getCanonicalLink,
Expand All @@ -32,8 +20,14 @@ import {
} from '../routing/compatibles'
import { findBrowserLocale, 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'

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

/**
* Returns a function to set i18n params.
Expand Down Expand Up @@ -328,7 +322,7 @@ export function useSwitchLocalePath(): SwitchLocalePathFunction {
*
* @public
*/
export function useBrowserLocale(normalizedLocales = nuxtI18nInternalOptions.__normalizedLocales): string | null {
export function useBrowserLocale(): string | null {
const headers = useRequestHeaders(['accept-language'])
return (
findBrowserLocale(
Expand All @@ -350,30 +344,24 @@ export function useBrowserLocale(normalizedLocales = nuxtI18nInternalOptions.__n
*
* @public
*/
export function useCookieLocale(
options: Required<Pick<DetectBrowserLanguageOptions, 'useCookie' | 'cookieKey'>> & {
localeCodes: readonly string[]
} = {
useCookie: nuxtI18nOptionsDefault.detectBrowserLanguage.useCookie,
cookieKey: nuxtI18nOptionsDefault.detectBrowserLanguage.cookieKey,
localeCodes: _localeCodes
}
): Ref<string> {
export function useCookieLocale(): Ref<string> {
// Support for importing from `#imports` is generated by auto `imports` nuxt module, so `ref` is imported from `vue`
const locale: Ref<string> = ref('')
const detect = nuxtI18nOptions.detectBrowserLanguage

if (detect && detect.useCookie) {
const cookieKey = detect.cookieKey

if (options.useCookie) {
let code: string | null = null
if (process.client) {
const cookie = useCookie<string>(options.cookieKey) as Ref<string>
code = cookie.value
code = useNuxtCookie<string>(cookieKey).value
} else if (process.server) {
const cookie = useRequestHeaders(['cookie'])
// eslint-disable-next-line @typescript-eslint/no-explicit-any
code = (cookie as any)[options.cookieKey]
code = (cookie as any)[cookieKey]
}

if (code && options.localeCodes.includes(code)) {
if (code && localeCodes.includes(code)) {
locale.value = code
}
}
Expand Down
Loading