Skip to content

Commit 7a38f86

Browse files
authored
feat: support SSR for v-t (#1087)
1 parent 33bbf7c commit 7a38f86

File tree

2 files changed

+135
-15
lines changed

2 files changed

+135
-15
lines changed

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import type {
1313
DirectiveBinding,
1414
ObjectDirective,
1515
WatchStopHandle,
16-
ComponentInternalInstance,
17-
VNode
16+
ComponentInternalInstance
1817
} from 'vue'
1918
import type { I18n, I18nInternal } from './i18n'
2019
import type { VueI18nInternal } from './legacy'
@@ -89,10 +88,8 @@ function getComposer(
8988
export type TranslationDirective<T = HTMLElement> = ObjectDirective<T>
9089

9190
export function vTDirective(i18n: I18n): TranslationDirective<HTMLElement> {
92-
const register = (
93-
el: HTMLElement,
94-
{ instance, value, modifiers }: DirectiveBinding
95-
): void => {
91+
function process(binding: DirectiveBinding): [string, Composer] {
92+
const { instance, modifiers, value } = binding
9693
/* istanbul ignore if */
9794
if (!instance || !instance.$) {
9895
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
@@ -104,16 +101,22 @@ export function vTDirective(i18n: I18n): TranslationDirective<HTMLElement> {
104101
}
105102

106103
const parsedValue = parseValue(value)
104+
return [
105+
Reflect.apply(composer.t, composer, [...makeParams(parsedValue)]),
106+
composer
107+
]
108+
}
109+
110+
const register = (el: HTMLElement, binding: DirectiveBinding): void => {
111+
const [textContent, composer] = process(binding)
107112
if (inBrowser && i18n.global === composer) {
108113
// global scope only
109114
el.__i18nWatcher = watch(composer.locale, () => {
110-
instance.$forceUpdate()
115+
binding.instance && binding.instance.$forceUpdate()
111116
})
112117
}
113118
el.__composer = composer
114-
el.textContent = Reflect.apply(composer.t, composer, [
115-
...makeParams(parsedValue)
116-
])
119+
el.textContent = textContent
117120
}
118121

119122
const unregister = (el: HTMLElement): void => {
@@ -142,10 +145,9 @@ export function vTDirective(i18n: I18n): TranslationDirective<HTMLElement> {
142145
created: register,
143146
unmounted: unregister,
144147
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')
148+
getSSRProps: (binding: DirectiveBinding) => {
149+
const [textContent] = process(binding)
150+
return { textContent }
149151
}
150152
} as TranslationDirective<HTMLElement>
151153
}

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

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { h, defineComponent, resolveComponent, createSSRApp } from 'vue'
1+
import {
2+
h,
3+
ref,
4+
defineComponent,
5+
resolveComponent,
6+
resolveDirective,
7+
withDirectives,
8+
createSSRApp
9+
} from 'vue'
210
import { renderToString } from '@vue/server-renderer'
311
import {
412
compileToFunction,
@@ -85,3 +93,113 @@ test('component: i18n-t', async () => {
8593

8694
expect(await renderToString(app)).toMatch(`<p>こんにちは!</p>`)
8795
})
96+
97+
describe('custom directive: v-t', () => {
98+
test('basic', async () => {
99+
const i18n = createI18n({
100+
legacy: false,
101+
locale: 'en',
102+
messages: {
103+
en: {
104+
hello: 'hello!'
105+
}
106+
}
107+
})
108+
109+
const App = defineComponent({
110+
setup() {
111+
// <p v-t="'hello'"></p>
112+
const t = resolveDirective('t')
113+
return () => {
114+
return withDirectives(h('p'), [[t!, 'hello']])
115+
}
116+
}
117+
})
118+
const app = createSSRApp(App)
119+
app.use(i18n)
120+
121+
expect(await renderToString(app)).toEqual('<p>hello!</p>')
122+
})
123+
124+
test('binding', async () => {
125+
const i18n = createI18n({
126+
locale: 'en',
127+
messages: {
128+
en: {
129+
hello: 'hello!'
130+
}
131+
}
132+
})
133+
134+
const App = defineComponent({
135+
setup() {
136+
// <p v-t="msg"></p>
137+
const msg = ref('hello')
138+
const t = resolveDirective('t')
139+
return () => {
140+
return withDirectives(h('p'), [[t!, msg.value]])
141+
}
142+
}
143+
})
144+
const app = createSSRApp(App)
145+
app.use(i18n)
146+
147+
expect(await renderToString(app)).toEqual('<p>hello!</p>')
148+
})
149+
150+
test('object literal', async () => {
151+
const i18n = createI18n({
152+
locale: 'en',
153+
messages: {
154+
en: {
155+
hello: 'hello, {name}!'
156+
},
157+
ja: {
158+
hello: 'こんにちは、{name}!'
159+
}
160+
}
161+
})
162+
163+
const App = defineComponent({
164+
setup() {
165+
// <p v-t="{ path: 'hello', locale: 'ja', args: { name: name.value } }"></p>
166+
const name = ref('kazupon')
167+
const t = resolveDirective('t')
168+
return () => {
169+
return withDirectives(h('p'), [
170+
[t!, { path: 'hello', locale: 'ja', args: { name: name.value } }]
171+
])
172+
}
173+
}
174+
})
175+
const app = createSSRApp(App)
176+
app.use(i18n)
177+
178+
expect(await renderToString(app)).toEqual('<p>こんにちは、kazupon!</p>')
179+
})
180+
181+
test('plural', async () => {
182+
const i18n = createI18n({
183+
locale: 'en',
184+
messages: {
185+
en: {
186+
banana: 'no bananas | {n} banana | {n} bananas'
187+
}
188+
}
189+
})
190+
191+
const App = defineComponent({
192+
setup() {
193+
// <p v-t="{ path: 'banana', choice: 2 }"></p>
194+
const t = resolveDirective('t')
195+
return () => {
196+
return withDirectives(h('p'), [[t!, { path: 'banana', choice: 2 }]])
197+
}
198+
}
199+
})
200+
const app = createSSRApp(App)
201+
app.use(i18n)
202+
203+
expect(await renderToString(app)).toEqual('<p>2 bananas</p>')
204+
})
205+
})

0 commit comments

Comments
 (0)