Skip to content

Commit 48eb688

Browse files
authored
feat: release global scope (#977)
1 parent 7960a1a commit 48eb688

File tree

2 files changed

+88
-11
lines changed

2 files changed

+88
-11
lines changed

packages/vue-i18n-core/src/i18n.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
shallowRef,
99
isRef,
1010
ref,
11-
computed
11+
computed,
12+
effectScope
1213
} from 'vue'
1314
import {
1415
inBrowser,
@@ -47,7 +48,7 @@ import {
4748
adjustI18nResources
4849
} from './utils'
4950

50-
import type { ComponentInternalInstance, App } from 'vue'
51+
import type { ComponentInternalInstance, App, EffectScope } from 'vue'
5152
import type {
5253
Locale,
5354
Path,
@@ -234,6 +235,10 @@ export interface I18n<
234235
* @param options - An install options
235236
*/
236237
install(app: App, ...options: unknown[]): void
238+
/**
239+
* Release global scope resource
240+
*/
241+
dispose(): void
237242
}
238243

239244
/**
@@ -492,7 +497,11 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
492497
? !!options.allowComposition
493498
: true
494499
const __instances = new Map<ComponentInternalInstance, VueI18n | Composer>()
495-
const __global = createGlobal(options, __legacyMode, VueI18nLegacy)
500+
const [globalScope, __global] = createGlobal(
501+
options,
502+
__legacyMode,
503+
VueI18nLegacy
504+
)
496505
const symbol: InjectionKey<I18n> | string = /* #__PURE__*/ makeSymbol(
497506
__DEV__ ? 'vue-i18n' : ''
498507
)
@@ -559,6 +568,13 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
559568
)
560569
}
561570

571+
// release global scope
572+
const unmountApp = app.unmount
573+
app.unmount = () => {
574+
i18n.dispose()
575+
unmountApp()
576+
}
577+
562578
// setup vue-devtools plugin
563579
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
564580
const ret = await enableDevTools(app, i18n as _I18n)
@@ -584,6 +600,9 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
584600
get global() {
585601
return __global
586602
},
603+
dispose(): void {
604+
globalScope.stop()
605+
},
587606
// @internal
588607
__instances,
589608
// @internal
@@ -598,6 +617,7 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
598617
// extend legacy VueI18n instance
599618

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

633653
if (!__legacyMode && __globalInjection) {
634-
injectGlobalFieldsForBridge(Vue, i18n, __global as Composer)
654+
_localeWatcher = injectGlobalFieldsForBridge(
655+
Vue,
656+
i18n,
657+
__global as Composer
658+
)
635659
}
636660
Vue.mixin(defineMixinBridge(i18n, _legacyVueI18n))
637661
}
638662
})
663+
Object.defineProperty(i18n, 'dispose', {
664+
value: (): void => {
665+
_localeWatcher && _localeWatcher()
666+
globalScope.stop()
667+
}
668+
})
639669
const methodMap = {
640670
__getInstance,
641671
__setInstance,
@@ -861,16 +891,26 @@ function createGlobal(
861891
options: I18nOptions,
862892
legacyMode: boolean,
863893
VueI18nLegacy: any // eslint-disable-line @typescript-eslint/no-explicit-any
864-
): VueI18n | Composer {
894+
): [EffectScope, VueI18n | Composer] {
895+
const scope = effectScope()
865896
if (!__BRIDGE__) {
866-
return !__LITE__ && __FEATURE_LEGACY_API__ && legacyMode
867-
? createVueI18n(options, VueI18nLegacy)
868-
: createComposer(options, VueI18nLegacy)
897+
const obj =
898+
!__LITE__ && __FEATURE_LEGACY_API__ && legacyMode
899+
? scope.run(() => createVueI18n(options, VueI18nLegacy))
900+
: scope.run(() => createComposer(options, VueI18nLegacy))
901+
if (obj == null) {
902+
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
903+
}
904+
return [scope, obj]
869905
} else {
870906
if (!isLegacyVueI18n(VueI18nLegacy)) {
871907
throw createI18nError(I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N)
872908
}
873-
return createComposer(options, VueI18nLegacy)
909+
const obj = scope.run(() => createComposer(options, VueI18nLegacy))
910+
if (obj == null) {
911+
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
912+
}
913+
return [scope, obj]
874914
}
875915
}
876916

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

15871627
// define fowardcompatible vue-i18n-next inject fields with `globalInjection`
15881628
Vue.prototype.$t = function (...args: unknown[]) {
@@ -1596,4 +1636,6 @@ function injectGlobalFieldsForBridge(
15961636
Vue.prototype.$n = function (...args: unknown[]) {
15971637
return Reflect.apply(composer.n, composer, [...args])
15981638
}
1639+
1640+
return watcher
15991641
}

packages/vue-i18n-core/test/i18n.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from '@intlify/devtools-if'
3333

3434
import type { I18n } from '../src/i18n'
35+
import type { App } from 'vue'
3536

3637
const container = document.createElement('div')
3738
document.body.appendChild(container)
@@ -1286,3 +1287,37 @@ describe('castToVueI18n', () => {
12861287
)
12871288
})
12881289
})
1290+
1291+
describe('release global scope', () => {
1292+
test('call dispose', () => {
1293+
let i18n: I18n | undefined
1294+
let error = ''
1295+
try {
1296+
i18n = createI18n({})
1297+
} catch (e) {
1298+
error = e.message
1299+
} finally {
1300+
i18n!.dispose()
1301+
}
1302+
expect(error).not.toEqual(errorMessages[I18nErrorCodes.UNEXPECTED_ERROR])
1303+
})
1304+
1305+
test('unmount', async () => {
1306+
let app: App | undefined
1307+
let error = ''
1308+
try {
1309+
const i18n = createI18n({
1310+
legacy: false,
1311+
locale: 'ja',
1312+
messages: {}
1313+
})
1314+
const wrapper = await mount({ template: '<p>unmound</p>' }, i18n)
1315+
app = wrapper.app
1316+
} catch (e) {
1317+
error = e.message
1318+
} finally {
1319+
app!.unmount()
1320+
}
1321+
expect(error).not.toEqual(errorMessages[I18nErrorCodes.UNEXPECTED_ERROR])
1322+
})
1323+
})

0 commit comments

Comments
 (0)