Skip to content

Commit 33bbf7c

Browse files
authored
fix: v-t directive reactivity (#1086)
* fix: v-t directive reactivity closes #1083 * refactor * remove * fix * fix eslint warnings
1 parent b01df6e commit 33bbf7c

File tree

2 files changed

+134
-7
lines changed

2 files changed

+134
-7
lines changed

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

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
import { watch } from 'vue'
12
import { I18nWarnCodes, getWarnMessage } from './warnings'
23
import { createI18nError, I18nErrorCodes } from './errors'
3-
import { isString, isPlainObject, isNumber, warn } from '@intlify/shared'
4+
import {
5+
isString,
6+
isPlainObject,
7+
isNumber,
8+
warn,
9+
inBrowser
10+
} from '@intlify/shared'
411

512
import type {
613
DirectiveBinding,
714
ObjectDirective,
8-
ComponentInternalInstance
15+
WatchStopHandle,
16+
ComponentInternalInstance,
17+
VNode
918
} from 'vue'
1019
import type { I18n, I18nInternal } from './i18n'
1120
import type { VueI18nInternal } from './legacy'
@@ -20,6 +29,13 @@ type VTDirectiveValue = {
2029
plural?: number
2130
}
2231

32+
declare global {
33+
interface HTMLElement {
34+
__i18nWatcher?: WatchStopHandle
35+
__composer?: Composer
36+
}
37+
}
38+
2339
function getComposer(
2440
i18n: I18n,
2541
instance: ComponentInternalInstance
@@ -73,7 +89,7 @@ function getComposer(
7389
export type TranslationDirective<T = HTMLElement> = ObjectDirective<T>
7490

7591
export function vTDirective(i18n: I18n): TranslationDirective<HTMLElement> {
76-
const bind = (
92+
const register = (
7793
el: HTMLElement,
7894
{ instance, value, modifiers }: DirectiveBinding
7995
): void => {
@@ -88,15 +104,49 @@ export function vTDirective(i18n: I18n): TranslationDirective<HTMLElement> {
88104
}
89105

90106
const parsedValue = parseValue(value)
91-
// el.textContent = composer.t(...makeParams(parsedValue))
107+
if (inBrowser && i18n.global === composer) {
108+
// global scope only
109+
el.__i18nWatcher = watch(composer.locale, () => {
110+
instance.$forceUpdate()
111+
})
112+
}
113+
el.__composer = composer
92114
el.textContent = Reflect.apply(composer.t, composer, [
93115
...makeParams(parsedValue)
94116
])
95117
}
96118

119+
const unregister = (el: HTMLElement): void => {
120+
if (inBrowser && el.__i18nWatcher) {
121+
el.__i18nWatcher()
122+
el.__i18nWatcher = undefined
123+
delete el.__i18nWatcher
124+
}
125+
if (el.__composer) {
126+
el.__composer = undefined
127+
delete el.__composer
128+
}
129+
}
130+
131+
const update = (el: HTMLElement, { value }: DirectiveBinding): void => {
132+
if (el.__composer) {
133+
const composer = el.__composer
134+
const parsedValue = parseValue(value)
135+
el.textContent = Reflect.apply(composer.t, composer, [
136+
...makeParams(parsedValue)
137+
])
138+
}
139+
}
140+
97141
return {
98-
beforeMount: bind,
99-
beforeUpdate: bind
142+
created: register,
143+
unmounted: unregister,
144+
beforeUpdate: update,
145+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
146+
getSSRProps: (binding: DirectiveBinding, vnode: VNode) => {
147+
// TODO: support SSR
148+
throw new Error('v-t still is not supported in SSR fully')
149+
}
100150
} as TranslationDirective<HTMLElement>
101151
}
102152

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ jest.mock('@intlify/shared', () => ({
99
}))
1010
import { warn } from '@intlify/shared'
1111

12-
import { ref, defineComponent } from 'vue'
12+
import {
13+
ref,
14+
defineComponent,
15+
h,
16+
withDirectives,
17+
resolveDirective,
18+
nextTick
19+
} from 'vue'
1320
import {
1421
setDevToolsHook,
1522
compileToFunction,
@@ -608,3 +615,73 @@ test('issue #1055', async () => {
608615

609616
expect(wrapper.html()).toMatchSnapshot()
610617
})
618+
619+
test('issue #1083', async () => {
620+
const i18n = createI18n({
621+
legacy: false,
622+
locale: 'en',
623+
messages: {
624+
en: {
625+
hello_world: 'Hello World!'
626+
},
627+
ja: {
628+
hello_world: 'こんにちは世界!'
629+
}
630+
}
631+
})
632+
633+
const LanguageSelector = defineComponent({
634+
setup() {
635+
const { locale, availableLocales } = useI18n({
636+
useScope: 'global'
637+
})
638+
function selectLocale(newLocale: string) {
639+
locale.value = newLocale
640+
}
641+
return { availableLocales, selectLocale, locale }
642+
},
643+
template: `<div>
644+
<div :id="l" v-for="l in availableLocales" @click="selectLocale(l)">
645+
{{ l }}
646+
</div>
647+
<p id="locale">{{ locale }}</p>
648+
<div>`
649+
})
650+
651+
const HelloWorld = defineComponent({
652+
setup() {
653+
const t = resolveDirective('t')
654+
return () => {
655+
return withDirectives(h('h1', { id: 'v-t' }), [
656+
[t!, { path: 'hello_world' }]
657+
])
658+
}
659+
}
660+
})
661+
662+
const App = defineComponent({
663+
components: {
664+
LanguageSelector,
665+
HelloWorld
666+
},
667+
template: `
668+
<HelloWorld />
669+
<LanguageSelector />
670+
`
671+
})
672+
673+
const wrapper = await mount(App, i18n)
674+
675+
const enEl = wrapper.rootEl.querySelector('#en')
676+
const jaEl = wrapper.rootEl.querySelector('#ja')
677+
const dirEl = wrapper.rootEl.querySelector('#v-t')
678+
expect(dirEl!.textContent).toEqual('Hello World!')
679+
680+
jaEl!.dispatchEvent(new Event('click'))
681+
await nextTick()
682+
expect(dirEl!.textContent).toEqual('こんにちは世界!')
683+
684+
enEl!.dispatchEvent(new Event('click'))
685+
await nextTick()
686+
expect(dirEl!.textContent).toEqual('Hello World!')
687+
})

0 commit comments

Comments
 (0)