Skip to content
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
64 changes: 53 additions & 11 deletions packages/vue-i18n-core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
shallowRef,
isRef,
ref,
computed
computed,
effectScope
} from 'vue'
import {
inBrowser,
Expand Down Expand Up @@ -47,7 +48,7 @@ import {
adjustI18nResources
} from './utils'

import type { ComponentInternalInstance, App } from 'vue'
import type { ComponentInternalInstance, App, EffectScope } from 'vue'
import type {
Locale,
Path,
Expand Down Expand Up @@ -234,6 +235,10 @@ export interface I18n<
* @param options - An install options
*/
install(app: App, ...options: unknown[]): void
/**
* Release global scope resource
*/
dispose(): void
}

/**
Expand Down Expand Up @@ -492,7 +497,11 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
? !!options.allowComposition
: true
const __instances = new Map<ComponentInternalInstance, VueI18n | Composer>()
const __global = createGlobal(options, __legacyMode, VueI18nLegacy)
const [globalScope, __global] = createGlobal(
options,
__legacyMode,
VueI18nLegacy
)
const symbol: InjectionKey<I18n> | string = /* #__PURE__*/ makeSymbol(
__DEV__ ? 'vue-i18n' : ''
)
Expand Down Expand Up @@ -559,6 +568,13 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
)
}

// release global scope
const unmountApp = app.unmount
app.unmount = () => {
i18n.dispose()
unmountApp()
}

// setup vue-devtools plugin
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
const ret = await enableDevTools(app, i18n as _I18n)
Expand All @@ -584,6 +600,9 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
get global() {
return __global
},
dispose(): void {
globalScope.stop()
},
// @internal
__instances,
// @internal
Expand All @@ -598,6 +617,7 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
// extend legacy VueI18n instance

const i18n = (__global as any)[LegacyInstanceSymbol] // eslint-disable-line @typescript-eslint/no-explicit-any
let _localeWatcher: Function | null = null
Object.defineProperty(i18n, 'global', {
get() {
return __global
Expand Down Expand Up @@ -631,11 +651,21 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
__FEATURE_FULL_INSTALL__ && applyBridge(Vue, ...options)

if (!__legacyMode && __globalInjection) {
injectGlobalFieldsForBridge(Vue, i18n, __global as Composer)
_localeWatcher = injectGlobalFieldsForBridge(
Vue,
i18n,
__global as Composer
)
}
Vue.mixin(defineMixinBridge(i18n, _legacyVueI18n))
}
})
Object.defineProperty(i18n, 'dispose', {
value: (): void => {
_localeWatcher && _localeWatcher()
globalScope.stop()
}
})
const methodMap = {
__getInstance,
__setInstance,
Expand Down Expand Up @@ -861,16 +891,26 @@ function createGlobal(
options: I18nOptions,
legacyMode: boolean,
VueI18nLegacy: any // eslint-disable-line @typescript-eslint/no-explicit-any
): VueI18n | Composer {
): [EffectScope, VueI18n | Composer] {
const scope = effectScope()
if (!__BRIDGE__) {
return !__LITE__ && __FEATURE_LEGACY_API__ && legacyMode
? createVueI18n(options, VueI18nLegacy)
: createComposer(options, VueI18nLegacy)
const obj =
!__LITE__ && __FEATURE_LEGACY_API__ && legacyMode
? scope.run(() => createVueI18n(options, VueI18nLegacy))
: scope.run(() => createComposer(options, VueI18nLegacy))
if (obj == null) {
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
}
return [scope, obj]
} else {
if (!isLegacyVueI18n(VueI18nLegacy)) {
throw createI18nError(I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N)
}
return createComposer(options, VueI18nLegacy)
const obj = scope.run(() => createComposer(options, VueI18nLegacy))
if (obj == null) {
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
}
return [scope, obj]
}
}

Expand Down Expand Up @@ -1578,11 +1618,11 @@ function injectGlobalFieldsForBridge(
Vue: any, // eslint-disable-line @typescript-eslint/no-explicit-any
i18n: any, // eslint-disable-line @typescript-eslint/no-explicit-any
composer: Composer
): void {
): Function {
// The composition mode in vue-i18n-bridge is `$18n` is the VueI18n instance.
// so we need to tell composer to change the locale.
// If we don't do, things like `$t` that are injected will not be reacted.
i18n.watchLocale(composer)
const watcher = i18n.watchLocale(composer) as Function

// define fowardcompatible vue-i18n-next inject fields with `globalInjection`
Vue.prototype.$t = function (...args: unknown[]) {
Expand All @@ -1596,4 +1636,6 @@ function injectGlobalFieldsForBridge(
Vue.prototype.$n = function (...args: unknown[]) {
return Reflect.apply(composer.n, composer, [...args])
}

return watcher
}
35 changes: 35 additions & 0 deletions packages/vue-i18n-core/test/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from '@intlify/devtools-if'

import type { I18n } from '../src/i18n'
import type { App } from 'vue'

const container = document.createElement('div')
document.body.appendChild(container)
Expand Down Expand Up @@ -1286,3 +1287,37 @@ describe('castToVueI18n', () => {
)
})
})

describe('release global scope', () => {
test('call dispose', () => {
let i18n: I18n | undefined
let error = ''
try {
i18n = createI18n({})
} catch (e) {
error = e.message
} finally {
i18n!.dispose()
}
expect(error).not.toEqual(errorMessages[I18nErrorCodes.UNEXPECTED_ERROR])
})

test('unmount', async () => {
let app: App | undefined
let error = ''
try {
const i18n = createI18n({
legacy: false,
locale: 'ja',
messages: {}
})
const wrapper = await mount({ template: '<p>unmound</p>' }, i18n)
app = wrapper.app
} catch (e) {
error = e.message
} finally {
app!.unmount()
}
expect(error).not.toEqual(errorMessages[I18nErrorCodes.UNEXPECTED_ERROR])
})
})