Skip to content

Commit

Permalink
refactor: improve function type safety (#3024)
Browse files Browse the repository at this point in the history
* refactor: improve function type safety

* refactor: restructure and improve types

* refactor: restructure into compatibility file

* refactor: add comments and reorder imports

* refactor: rename method and reorder imports

* refactor: improve messages function types

* refactor: improve server plugin types
  • Loading branch information
BobbieGoede authored Jul 12, 2024
1 parent 2887904 commit 5e26133
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 217 deletions.
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

0 comments on commit 5e26133

Please sign in to comment.