Skip to content

Commit bcdd82a

Browse files
authored
Add support for binding SSR props in Vue 3 directive (#869)
1 parent 256538d commit bcdd82a

File tree

4 files changed

+103
-0
lines changed

4 files changed

+103
-0
lines changed

__tests__/utils/ssr.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as TestUtils from '@vue/test-utils'
2+
import type { ComponentOptions } from 'vue-3'
3+
import { install } from 'vue-demi'
4+
5+
import type { FluentVue } from '../../src'
6+
7+
install()
8+
9+
export function renderSSR<T extends object>(
10+
fluent: FluentVue | null,
11+
component: ComponentOptions<T>,
12+
): Promise<string> {
13+
const { renderToString } = TestUtils
14+
15+
const plugins = fluent ? [fluent] : []
16+
17+
return renderToString(component, {
18+
global: {
19+
plugins,
20+
},
21+
})
22+
}

__tests__/vue/directive.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ describe('directive', () => {
141141
`),
142142
)
143143

144+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
145+
144146
const component = {
145147
data: () => ({
146148
name: 'John',
@@ -153,6 +155,10 @@ describe('directive', () => {
153155

154156
// Assert
155157
expect(mounted.html()).toEqual('<a aria-label="Hello \u{2068}John\u{2069}">Text</a>')
158+
expect(warn).toHaveBeenCalledTimes(1)
159+
expect(warn).toHaveBeenCalledWith(
160+
'[fluent-vue] Attribute \'not-allowed\' on element <a> is not localizable. Remove it from the translation. Translation key: link',
161+
)
156162
})
157163

158164
it('works without fallbacks', () => {

__tests__/vue/ssr.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { beforeEach, describe, expect, it } from 'vitest'
2+
import { isVue3 } from 'vue-demi'
3+
4+
import { FluentBundle, FluentResource } from '@fluent/bundle'
5+
import ftl from '@fluent/dedent'
6+
7+
import { renderSSR } from '../utils/ssr'
8+
9+
import type { FluentVue } from '../../src'
10+
import { createFluentVue } from '../../src'
11+
12+
describe.skipIf(!isVue3)('sSR directive', () => {
13+
let fluent: FluentVue
14+
let bundle: FluentBundle
15+
16+
beforeEach(() => {
17+
bundle = new FluentBundle('en-US')
18+
19+
fluent = createFluentVue({
20+
bundles: [bundle],
21+
})
22+
})
23+
24+
it('translates text content', async () => {
25+
// Arrange
26+
bundle.addResource(
27+
new FluentResource(ftl`
28+
link = Link text
29+
.aria-label = Link aria label
30+
`),
31+
)
32+
33+
const component = {
34+
data: () => ({
35+
name: 'John',
36+
}),
37+
template: '<a v-t:link href="/foo">Fallback text</a>',
38+
}
39+
40+
// Act
41+
const rendered = await renderSSR(fluent, component)
42+
43+
// Assert
44+
// This has fallback text because the textContent is not supported by Vue getSSRProps
45+
// Text will be translated using directive transform
46+
expect(rendered).toEqual('<a href="/foo" aria-label="Link aria label">Fallback text</a>')
47+
})
48+
})

src/vue/directive.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ function translate(el: HTMLElement, fluent: TranslationContext, binding: VueDire
2323
for (const [attr, attrValue] of Object.entries(translation.attributes)) {
2424
if (isAttrNameLocalizable(attr, el, allowedAttrs))
2525
el.setAttribute(attr, attrValue)
26+
else
27+
warn(`Attribute '${attr}' on element <${el.tagName.toLowerCase()}> is not localizable. Remove it from the translation. Translation key: ${key}`)
2628
}
2729
}
2830

@@ -39,6 +41,31 @@ export function createVue3Directive(rootContext: TranslationContext): Vue3Direct
3941
const context = getContext(rootContext, binding.instance)
4042
translate(el, context, binding)
4143
},
44+
45+
getSSRProps(binding) {
46+
const context = getContext(rootContext, binding.instance)
47+
const key = binding.arg
48+
if (key === void 0) {
49+
warn('v-t directive is missing arg with translation key')
50+
return {}
51+
}
52+
const translation = context.formatWithAttrs(key, binding.value)
53+
const allowedAttrs = Object.keys(binding.modifiers)
54+
const attrs: Record<string, string> = {}
55+
for (const [attr, attrValue] of Object.entries(translation.attributes)) {
56+
// Vue 3 does not expose the element in the binding object
57+
// so we can't check if the attribute is allowed
58+
// we assume that all attributes are allowed
59+
// this could lead to SSR hydration mismatches if translation
60+
// contains attributes that are not allowed
61+
// There is a runtime warning in the browser console in case translation contains not allowed attributes
62+
if (isAttrNameLocalizable(attr, {} as HTMLElement, allowedAttrs))
63+
attrs[attr] = attrValue
64+
}
65+
66+
// TODO: Include textContent when https://github.com/vuejs/core/issues/8112 is resolved
67+
return attrs
68+
},
4269
}
4370
}
4471

0 commit comments

Comments
 (0)