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

refactor: improve function type safety #3024

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
116 changes: 116 additions & 0 deletions src/runtime/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Utility functions to support both VueI18n and Composer instances
*/

import { isRef, unref } from 'vue'

import type { NuxtApp } from '#app'
import type { LocaleObject } from '#build/i18n.options.mjs'
import type { Composer, I18n, Locale, VueI18n } from 'vue-i18n'
import type { UnwrapRef } from 'vue'

function isI18nInstance(i18n: I18n | VueI18n | Composer): i18n is I18n {
return i18n != null && 'global' in i18n && 'mode' in i18n
}

function isComposer(target: I18n | VueI18n | Composer): target is Composer {
return target != null && !('__composer' in target) && 'locale' in target && isRef(target.locale)
}

export function isVueI18n(target: I18n | VueI18n | Composer): target is VueI18n {
return target != null && '__composer' in target
}

export function getI18nTarget(i18n: I18n | VueI18n | Composer) {
return isI18nInstance(i18n) ? i18n.global : i18n
}

export function getComposer(i18n: I18n | VueI18n | Composer): Composer {
const target = getI18nTarget(i18n)

if (isComposer(target)) return target
if (isVueI18n(target)) return target.__composer

return target
}

/**
* Extract the value of a property on a VueI18n or Composer instance
*/
function extractI18nProperty<T extends ReturnType<typeof getI18nTarget>, K extends keyof T>(
i18n: T,
key: K
): UnwrapRef<T[K]> {
return unref(i18n[key]) as UnwrapRef<T[K]>
}

/**
* Typesafe access to property of a VueI18n or Composer instance
*/
export function getI18nProperty<K extends keyof ReturnType<typeof getI18nTarget>>(i18n: I18n, property: K) {
return extractI18nProperty(getI18nTarget(i18n), property)
}

/**
* Sets the value of the locale property on VueI18n or Composer instance
*
* This differs from the instance `setLocale` method in that it sets the
* locale property directly without triggering other side effects
*/
export function setLocaleProperty(i18n: I18n, locale: Locale): void {
const target = getI18nTarget(i18n)
if (isRef(target.locale)) {
target.locale.value = locale
} else {
target.locale = locale
}
}

export function getLocale(i18n: I18n): Locale {
return getI18nProperty(i18n, 'locale')
}

export function getLocales(i18n: I18n): string[] | LocaleObject[] {
return getI18nProperty(i18n, 'locales')
}

export function getLocaleCodes(i18n: I18n): string[] {
return getI18nProperty(i18n, 'localeCodes')
}

export function setLocale(i18n: I18n, locale: Locale) {
return getI18nTarget(i18n).setLocale(locale)
}

export function setLocaleCookie(i18n: I18n, locale: Locale) {
return getI18nTarget(i18n).setLocaleCookie(locale)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mergeLocaleMessage(i18n: I18n, locale: Locale, messages: Record<string, any>) {
return getI18nTarget(i18n).mergeLocaleMessage(locale, messages)
}

export async function onBeforeLanguageSwitch(
i18n: I18n,
oldLocale: string,
newLocale: string,
initial: boolean,
context: NuxtApp
) {
return getI18nTarget(i18n).onBeforeLanguageSwitch(oldLocale, newLocale, initial, context)
}

export function onLanguageSwitched(i18n: I18n, oldLocale: string, newLocale: string) {
return getI18nTarget(i18n).onLanguageSwitched(oldLocale, newLocale)
}

declare module 'vue-i18n' {
interface VueI18n {
/**
* This is not exposed in VueI18n's types, but it's used internally
* @internal
*/
__composer: Composer
}
}
7 changes: 4 additions & 3 deletions src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
localeRoute,
switchLocalePath
} from '../routing/compatibles'
import { findBrowserLocale, getComposer, getLocale, getLocales } from '../routing/utils'
import { findBrowserLocale } from '../routing/utils'
import { getLocale, getLocales, getComposer } from '../compatibility'

import type { Ref } from 'vue'
import type { Locale } from 'vue-i18n'
Expand All @@ -44,8 +45,8 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP
const i18n = getComposer(common.i18n)
const router = common.router

const locale = getLocale(i18n)
const locales = getNormalizedLocales(getLocales(i18n))
const locale = getLocale(common.i18n)
const locales = getNormalizedLocales(getLocales(common.i18n))
const _i18nParams = ref({})
const experimentalSSR = common.runtimeConfig.public.i18n.experimental.switchLocalePathLinkSSR

Expand Down
28 changes: 3 additions & 25 deletions src/runtime/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,12 @@
import { isArray, isString, isObject } from '@intlify/shared'
import { hasProtocol } from 'ufo'
import isHTTPS from 'is-https'
import {
useRequestHeaders,
useRequestEvent,
useCookie as useNuxtCookie,
useRuntimeConfig,
useNuxtApp,
unref
} from '#imports'
import { useRequestHeaders, useRequestEvent, useCookie as useNuxtCookie, useRuntimeConfig, useNuxtApp } from '#imports'
import { NUXT_I18N_MODULE_ID, DEFAULT_COOKIE_KEY, isSSG, localeCodes, normalizedLocales } from '#build/i18n.options.mjs'
import { findBrowserLocale, getLocalesRegex, getI18nTarget } from './routing/utils'
import { findBrowserLocale, getLocalesRegex } from './routing/utils'
import { initCommonComposableOptions, type CommonComposableOptions } from './utils'

import type { Locale, I18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
import type { DetectBrowserLanguageOptions, LocaleObject } from '#build/i18n.options.mjs'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import type { CookieRef } from 'nuxt/app'
Expand All @@ -25,21 +18,6 @@ export function formatMessage(message: string) {
return NUXT_I18N_MODULE_ID + ' ' + message
}

export function callVueI18nInterfaces<Return = any>(i18n: any, name: string, ...args: any[]): Return {
const target = getI18nTarget(i18n as unknown as I18n)
// prettier-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/ban-types
const [obj, method] = [target, (target as any)[name] as Function]
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
return Reflect.apply(method, obj, [...args]) as Return
}

export function getVueI18nPropertyValue<Return = any>(i18n: any, name: string): Return {
const target = getI18nTarget(i18n as unknown as I18n)
// @ts-expect-error name should be typed instead of string
return unref(target[name]) as Return
}

export function defineGetter<K extends string | number | symbol, V>(obj: Record<K, V>, key: K, val: V) {
Object.defineProperty(obj, key, { get: () => val })
}
Expand Down
29 changes: 14 additions & 15 deletions src/runtime/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import type { DeepRequired } from 'ts-essentials'
import type { VueI18nConfig, NuxtI18nOptions } from '../types'
import type { CoreContext } from '@intlify/h3'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LocaleLoader = { key: string; load: () => Promise<any>; cache: boolean }
type MessageLoaderFunction<T = DefineLocaleMessage> = (locale: Locale) => Promise<LocaleMessages<T>>
type MessageLoaderResult<T, Result = MessageLoaderFunction<T> | LocaleMessages<T>> = { default: Result } | Result

export type LocaleLoader<T = LocaleMessages<DefineLocaleMessage>> = {
key: string
load: () => Promise<MessageLoaderResult<T>>
cache: boolean
}
const cacheMessages = new Map<string, LocaleMessages<DefineLocaleMessage>>()

export async function loadVueI18nOptions(
Expand Down Expand Up @@ -73,21 +78,18 @@ async function loadMessage(locale: Locale, { key, load }: LocaleLoader) {
let message: LocaleMessages<DefineLocaleMessage> | null = null
try {
__DEBUG__ && console.log('loadMessage: (locale) -', locale)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- FIXME
const getter = await load().then(r => r.default || r)
const getter = await load().then(r => ('default' in r ? r.default : r))
if (isFunction(getter)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- FIXME
message = await getter(locale)
__DEBUG__ && console.log('loadMessage: dynamic load', message)
} else {
message = getter as LocaleMessages<DefineLocaleMessage>
message = getter
if (message != null && cacheMessages) {
cacheMessages.set(key, message)
}
__DEBUG__ && console.log('loadMessage: load', message)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
} catch (e: unknown) {
console.error('Failed locale loading: ' + (e as Error).message)
}
return message
Expand Down Expand Up @@ -126,20 +128,17 @@ export async function loadLocale(
setter(locale, targetMessage)
}

type LocaleLoaderMessages = CoreContext['messages'] | LocaleMessages<DefineLocaleMessage>
type LocaleLoaderMessages =
| CoreContext<Locale, DefineLocaleMessage>['messages']
| LocaleMessages<DefineLocaleMessage, Locale>
export async function loadAndSetLocaleMessages(
locale: Locale,
localeLoaders: Record<Locale, LocaleLoader[]>,
messages: LocaleLoaderMessages
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setter = (locale: Locale, message: Record<string, any>) => {
// @ts-expect-error should be able to use `locale` as index
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- FIXME
const setter = (locale: Locale, message: LocaleMessages<DefineLocaleMessage, Locale>) => {
const base = messages[locale] || {}
deepCopy(message, base)
// @ts-expect-error should be able to use `locale` as index
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- FIXME
messages[locale] = base
}

Expand Down
41 changes: 16 additions & 25 deletions src/runtime/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,19 @@ import {
normalizedLocales
} from '#build/i18n.options.mjs'
import { loadVueI18nOptions, loadInitialMessages, loadLocale } from '../messages'
import { loadAndSetLocale, detectLocale, detectRedirect, navigate, injectNuxtHelpers, extendBaseUrl } from '../utils'
import {
loadAndSetLocale,
detectLocale,
detectRedirect,
navigate,
injectNuxtHelpers,
extendBaseUrl,
_setLocale,
mergeLocaleMessage
} from '../utils'
import {
getBrowserLocale as _getBrowserLocale,
getLocaleCookie as _getLocaleCookie,
setLocaleCookie as _setLocaleCookie,
getBrowserLocale,
getLocaleCookie,
setLocaleCookie,
detectBrowserLanguage,
DefaultDetectBrowserLanguageFromResult,
getI18nCookie,
runtimeDetectBrowserLanguage
} from '../internal'
import { getLocale, inBrowser, resolveBaseUrl, setLocale } from '../routing/utils'
import { inBrowser, resolveBaseUrl } from '../routing/utils'
import { extendI18n, createLocaleFromRouteGetter } from '../routing/extends'
import { setLocale, getLocale, mergeLocaleMessage, setLocaleProperty } from '../compatibility'

import type { LocaleObject } from '#build/i18n.options.mjs'
import type { Locale, I18nOptions } from 'vue-i18n'
Expand Down Expand Up @@ -81,7 +73,7 @@ export default defineNuxtPlugin({
ssg: isSSG && runtimeI18n.strategy === 'no_prefix' ? 'ssg_ignore' : 'normal',
callType: 'setup',
firstAccess: true,
localeCookie: _getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
localeCookie: getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
},
runtimeI18n
)
Expand Down Expand Up @@ -118,7 +110,7 @@ export default defineNuxtPlugin({
* avoid hydration mismatch for SSG mode
*/
if (isSSGModeInitialSetup() && runtimeI18n.strategy === 'no_prefix' && import.meta.client) {
nuxt.hook('app:mounted', () => {
nuxt.hook('app:mounted', async () => {
__DEBUG__ && console.log('hook app:mounted')
const {
locale: browserLocale,
Expand All @@ -133,7 +125,7 @@ export default defineNuxtPlugin({
ssg: 'ssg_setup',
callType: 'setup',
firstAccess: true,
localeCookie: _getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
localeCookie: getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
},
initialLocale
)
Expand All @@ -146,7 +138,7 @@ export default defineNuxtPlugin({
reason,
from
)
_setLocale(i18n, browserLocale)
await setLocale(i18n, browserLocale)
ssgModeInitialSetup = false
})
}
Expand Down Expand Up @@ -211,16 +203,15 @@ export default defineNuxtPlugin({
)
}
composer.loadLocaleMessages = async (locale: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setter = (locale: Locale, message: Record<string, any>) => mergeLocaleMessage(i18n, locale, message)
const setter = mergeLocaleMessage.bind(null, i18n)
await loadLocale(locale, localeLoaders, setter)
}
composer.differentDomains = runtimeI18n.differentDomains
composer.defaultLocale = runtimeI18n.defaultLocale
composer.getBrowserLocale = () => _getBrowserLocale()
composer.getBrowserLocale = () => getBrowserLocale()
composer.getLocaleCookie = () =>
_getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
composer.setLocaleCookie = (locale: string) => _setLocaleCookie(localeCookie, locale, _detectBrowserLanguage)
getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
composer.setLocaleCookie = (locale: string) => setLocaleCookie(localeCookie, locale, _detectBrowserLanguage)

composer.onBeforeLanguageSwitch = (oldLocale, newLocale, initialSetup, context) =>
nuxt.callHook('i18n:beforeLocaleSwitch', { oldLocale, newLocale, initialSetup, context }) as Promise<void>
Expand All @@ -231,7 +222,7 @@ export default defineNuxtPlugin({
if (!i18n.__pendingLocale) {
return
}
setLocale(i18n, i18n.__pendingLocale)
setLocaleProperty(i18n, i18n.__pendingLocale)
if (i18n.__resolvePendingLocalePromise) {
// eslint-disable-next-line @typescript-eslint/await-thenable -- FIXME: `__resolvePendingLocalePromise` should be `Promise<void>`
await i18n.__resolvePendingLocalePromise()
Expand Down Expand Up @@ -330,7 +321,7 @@ export default defineNuxtPlugin({
ssg: isSSGModeInitialSetup() && runtimeI18n.strategy === 'no_prefix' ? 'ssg_ignore' : 'normal',
callType: 'routing',
firstAccess: routeChangeCount === 0,
localeCookie: _getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
localeCookie: getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
},
runtimeI18n
)
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/routing/compatibles/head.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { joinURL } from 'ufo'
import { isArray, isObject } from '@intlify/shared'
import { unref, useNuxtApp, useRuntimeConfig } from '#imports'

import { getComposer, getLocale, getLocales, getNormalizedLocales } from '../utils'
import { getNormalizedLocales } from '../utils'
import { getRouteBaseName, localeRoute, switchLocalePath } from './routing'
import { isArray, isObject } from '@intlify/shared'
import { joinURL } from 'ufo'
import { getComposer, getLocale, getLocales } from '../../compatibility'

import type { I18n } from 'vue-i18n'
import type { I18nHeadMetaInfo, MetaAttrs, LocaleObject, I18nHeadOptions } from '#build/i18n.options.mjs'
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/routing/compatibles/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { hasProtocol, parsePath, parseQuery, withTrailingSlash, withoutTrailingS
import { DEFAULT_DYNAMIC_PARAMS_KEY } from '#build/i18n.options.mjs'
import { unref } from '#imports'

import { getLocale } from '../../compatibility'
import { resolve, routeToObject } from './utils'
import { getLocale, getLocaleRouteName, getRouteName } from '../utils'
import { getLocaleRouteName, getRouteName } from '../utils'
import { extendPrefixable, extendSwitchLocalePathIntercepter, type CommonComposableOptions } from '../../utils'

import type { Strategies, PrefixableOptions, SwitchLocalePathIntercepter } from '#build/i18n.options.mjs'
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/routing/extends/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { effectScope } from '#imports'
import { isVueI18n, getComposer } from '../utils'
import { isVueI18n, getComposer } from '../../compatibility'
import {
getRouteBaseName,
localeHead,
Expand Down
Loading