Skip to content
This repository was archived by the owner on Dec 31, 2024. It is now read-only.

Commit 0f0f3ff

Browse files
myst729kazupon
authored andcommitted
⭐ new(interpolation): list formatting refactor and places/place feature (#218) by @myst729
* fix: test case typo * improvement(interpolation): enable array-like named values for list tokens * feature(component): place and places prop for component interpolation * docs(formatting): enable array-like object values for list formatting * docs(component): named formatting with place/places
1 parent ccf4c0a commit 0f0f3ff

7 files changed

Lines changed: 251 additions & 37 deletions

File tree

gitbook/en/formatting.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ Output the below:
8282
<p>hello world</p>
8383
```
8484

85+
List formatting also accepts array-like object:
86+
87+
```html
88+
<p>{{ $t('message.hello', {'0': 'hello'}) }}</p>
89+
```
90+
91+
Output the below:
92+
93+
```html
94+
<p>hello world</p>
95+
```
96+
8597
## Support ruby on rails i18n format
8698

8799
Locale messages the below:

gitbook/en/interpolation.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,81 @@ About the above example, see the [example](https://github.com/kazupon/vue-i18n/t
8585

8686
The children of `i18n` functional component is interpolated with locale message of `path` prop. In the above example, `<a :href="url" target="_blank">{{ $t('tos') }}</a>` is interplated with `term` locale message.
8787

88-
The component interpolations follows the **list formatting**. The named formatting is not support. The children of `i18n` functional component is interpolated with order of list formatting.
88+
In above example, the component interpolation follows the **list formatting**. The children of `i18n` functional component are interpolated by their orders of appearance.
89+
90+
> :warning: NOTE: In `i18n` component, text content consists of only white spaces will be omitted.
91+
92+
Named formatting is supported with the help of `place` attribute. For example:
93+
94+
```html
95+
<div id="app">
96+
<!-- ... -->
97+
<i18n path="info" tag="p">
98+
<span place="limit">{{ changeLimit }}</span>
99+
<a place="action" :href="changeUrl">{{ $t('change') }}</a>
100+
</i18n>
101+
<!-- ... -->
102+
</div>
103+
```
104+
105+
```javascript
106+
const messages = {
107+
en: {
108+
info: 'You can {action} until {limit} minutes from departure.',
109+
change: 'change your flight',
110+
refund: 'refund the ticket'
111+
}
112+
}
113+
114+
const i18n = new VueI18n({
115+
locale: 'en',
116+
messages
117+
})
118+
new Vue({
119+
i18n,
120+
data: {
121+
changeUrl: '/change',
122+
refundUrl: '/refund',
123+
changeLimit: 15,
124+
refundLimit: 30
125+
}
126+
}).$mount('#app')
127+
```
128+
129+
Outputs:
130+
131+
```html
132+
<div id="app">
133+
<!-- ... -->
134+
<p>
135+
You can <a href="/change">change your flight</a> until <span>15</span> minutes from departure.
136+
</p>
137+
<!-- ... -->
138+
</div>
139+
```
140+
141+
> :warning: NOTE: To use named formatting, all children of `i18n` component must have `place` attribute set. Otherwise it will fallback to list formatting.
142+
143+
If you still want to interpolate text content in named formatting, you could define `places` property on `i18n` component. For example:
144+
145+
```html
146+
<div id="app">
147+
<!-- ... -->
148+
<i18n path="info" tag="p" :places="{ limit: refundLimit }">
149+
<a place="action" :href="refundUrl">{{ $t('refund') }}</a>
150+
</i18n>
151+
<!-- ... -->
152+
</div>
153+
```
154+
155+
Outputs:
156+
157+
```html
158+
<div id="app">
159+
<!-- ... -->
160+
<p>
161+
You can <a href="/refund">refund your ticket</a> until 30 minutes from departure.
162+
</p>
163+
<!-- ... -->
164+
</div>
165+
```

src/component.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ export default {
1616
},
1717
locale: {
1818
type: String
19+
},
20+
places: {
21+
type: [Array, Object]
1922
}
2023
},
2124
render (h: Function, { props, data, children, parent }: Object) {
2225
const i18n = parent.$i18n
26+
27+
children = (children || []).filter(child => {
28+
return child.tag || (child.text = child.text.trim())
29+
})
30+
2331
if (!i18n) {
2432
if (process.env.NODE_ENV !== 'production') {
2533
warn('Cannot find VueI18n instance!')
@@ -30,14 +38,41 @@ export default {
3038
const path: Path = props.path
3139
const locale: ?Locale = props.locale
3240

33-
const params: Array<any> = []
34-
locale && params.push(locale)
35-
children.forEach(child => {
36-
if (child.tag || child.text.trim()) {
37-
params.push(child)
41+
const params: Object = {}
42+
const places: Array<any> | Object = props.places || {}
43+
44+
const hasPlaces: boolean = Array.isArray(places)
45+
? places.length > 0
46+
: Object.keys(places).length > 0
47+
48+
const everyPlace: boolean = children.every(child => {
49+
if (child.data && child.data.attrs) {
50+
const place = child.data.attrs.place
51+
return (typeof place !== 'undefined') && place !== ''
3852
}
3953
})
4054

41-
return h(props.tag, data, i18n.i(path, ...params))
55+
if (hasPlaces && children.length > 0 && !everyPlace) {
56+
warn('If places prop is set, all child elements must have place prop set.')
57+
}
58+
59+
if (Array.isArray(places)) {
60+
places.forEach((el, i) => {
61+
params[i] = el
62+
})
63+
} else {
64+
Object.keys(places).forEach(key => {
65+
params[key] = places[key]
66+
})
67+
}
68+
69+
children.forEach((child, i: number) => {
70+
const key: string = everyPlace
71+
? `${child.data.attrs.place}`
72+
: `${i}`
73+
params[key] = child
74+
})
75+
76+
return h(props.tag, data, i18n.i(path, locale, params))
4277
}
4378
}

src/format.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,7 @@ export function compile (tokens: Array<Token>, values: Object | Array<any>): Arr
8686
compiled.push(token.value)
8787
break
8888
case 'list':
89-
if (mode === 'list') {
90-
compiled.push(values[parseInt(token.value, 10)])
91-
} else {
92-
if (process.env.NODE_ENV !== 'production') {
93-
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
94-
}
95-
}
89+
compiled.push(values[parseInt(token.value, 10)])
9690
break
9791
case 'named':
9892
if (mode === 'named') {

src/index.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -321,37 +321,29 @@ export default class VueI18n {
321321
return this._t(key, this.locale, this._getMessages(), null, ...values)
322322
}
323323

324-
_i (key: Path, locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
324+
_i (key: Path, locale: Locale, messages: LocaleMessages, host: any, values: Object): any {
325325
const ret: any =
326326
this._translate(messages, locale, this.fallbackLocale, key, host, 'raw', values)
327327
if (this._isFallbackRoot(ret)) {
328328
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
329329
warn(`Fall back to interpolate the keypath '${key}' with root locale.`)
330330
}
331331
if (!this._root) { throw Error('unexpected error') }
332-
return this._root.i(key, ...values)
332+
return this._root.i(key, locale, values)
333333
} else {
334334
return this._warnDefault(locale, key, ret, host)
335335
}
336336
}
337337

338-
i (key: Path, ...values: any): TranslateResult {
338+
i (key: Path, locale: Locale, values: Object): TranslateResult {
339339
/* istanbul ignore if */
340340
if (!key) { return '' }
341341

342-
let locale: Locale = this.locale
343-
let index: number = 0
344-
if (typeof values[0] === 'string') {
345-
locale = values[0]
346-
index = 1
347-
}
348-
349-
const params: Array<any> = []
350-
for (let i = index; i < values.length; i++) {
351-
params.push(values[i])
342+
if (typeof locale !== 'string') {
343+
locale = this.locale
352344
}
353345

354-
return this._i(key, locale, this._getMessages(), null, ...params)
346+
return this._i(key, locale, this._getMessages(), null, values)
355347
}
356348

357349
_tc (

test/unit/format.test.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,27 @@ describe('compile', () => {
110110
})
111111
})
112112

113+
describe('list token with named value', () => {
114+
it('should be compiled', () => {
115+
const tokens = parse('name: {0}, age: {1}') // list tokens
116+
const compiled = compile(tokens, { '0': 'kazupon', '1': '0x20' }) // named values
117+
assert(compiled.length === 4)
118+
assert.equal(compiled[0], 'name: ')
119+
assert.equal(compiled[1], 'kazupon')
120+
assert.equal(compiled[2], ', age: ')
121+
assert.equal(compiled[3], '0x20')
122+
})
123+
})
124+
113125
describe('unmatch values mode', () => {
114126
it('should be warned', () => {
115127
const spy = sinon.spy(console, 'warn')
116128

117-
const tokens1 = parse('name: {0}, age: {1}') // list tokens
118-
compile(tokens1, { name: 'kazupon', age: '0x20' }) // named values
119-
const tokens2 = parse('name: {name}, age: {age}') // named tokens
120-
compile(tokens2, ['kazupon', '0x20']) // list values
129+
const tokens = parse('name: {name}, age: {age}') // named tokens
130+
compile(tokens, ['kazupon', '0x20']) // list values
121131

122132
assert(spy.notCalled === false)
123-
assert(spy.callCount === 4)
133+
assert(spy.callCount === 2)
124134
spy.restore()
125135
})
126136
})

test/unit/interpolation.test.js

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import Component from '../../src/component'
33
const messages = {
44
en: {
55
text: 'one: {0}',
6-
premitive: 'one: {0}, two: {1}',
6+
primitive: 'one: {0}, two: {1}',
77
component: 'element: {0}, component: {1}',
8-
link: '@:premitive',
8+
mixed: 'text: {x}, component: {y}',
9+
link: '@:primitive',
910
term: 'I accept xxx {0}.',
1011
tos: 'Term of service',
1112
fallback: 'fallback from {0}'
@@ -60,13 +61,13 @@ describe('component interpolation', () => {
6061
})
6162
})
6263

63-
describe('premitive nodes', () => {
64+
describe('primitive nodes', () => {
6465
it('should be interpolated', done => {
6566
const el = document.createElement('div')
6667
const vm = new Vue({
6768
i18n,
6869
render (h) {
69-
return h('i18n', { props: { path: 'premitive' } }, [
70+
return h('i18n', { props: { path: 'primitive' } }, [
7071
h('p', ['1']),
7172
h('p', ['2'])
7273
])
@@ -97,6 +98,99 @@ describe('component interpolation', () => {
9798
})
9899
})
99100

101+
describe('places prop', () => {
102+
it('should be interpolated', done => {
103+
const el = document.createElement('div')
104+
const vm = new Vue({
105+
i18n,
106+
render (h) {
107+
return h('i18n', { props: { path: 'text', places: [1] } })
108+
}
109+
}).$mount(el)
110+
nextTick(() => {
111+
assert.equal(vm.$el.textContent, 'one: 1')
112+
}).then(done)
113+
})
114+
})
115+
116+
describe('place prop on all children', () => {
117+
it('should be interpolated', done => {
118+
const el = document.createElement('div')
119+
const vm = new Vue({
120+
i18n,
121+
components,
122+
render (h) {
123+
return h('i18n', { props: { path: 'component' } }, [
124+
h('p', { props: { place: 0 } }, ['1']),
125+
h('comp', { props: { place: 1, msg: 'foo' } })
126+
])
127+
}
128+
}).$mount(el)
129+
nextTick(() => {
130+
assert.equal(vm.$el.innerHTML, 'element: <p>1</p>, component: <p>foo</p>')
131+
}).then(done)
132+
})
133+
})
134+
135+
describe('place prop on some children', () => {
136+
it('should be interpolated', done => {
137+
const el = document.createElement('div')
138+
const vm = new Vue({
139+
i18n,
140+
components,
141+
render (h) {
142+
return h('i18n', { props: { path: 'component' } }, [
143+
h('p', { props: { place: 1 } }, ['1']),
144+
h('comp', { props: { msg: 'foo' } })
145+
])
146+
}
147+
}).$mount(el)
148+
nextTick(() => {
149+
assert.equal(vm.$el.innerHTML, 'element: <p>1</p>, component: <p>foo</p>')
150+
}).then(done)
151+
})
152+
})
153+
154+
describe('places and place mixed', () => {
155+
it('should be interpolated', done => {
156+
const el = document.createElement('div')
157+
const vm = new Vue({
158+
i18n,
159+
components,
160+
render (h) {
161+
return h('i18n', { props: { path: 'mixed', places: { 'x': 'foo' } } }, [
162+
h('comp', { props: { msg: 'bar' }, attrs: { place: 'y' } })
163+
])
164+
}
165+
}).$mount(el)
166+
nextTick(() => {
167+
assert.equal(vm.$el.innerHTML, 'text: foo, component: <p place="y">bar</p>')
168+
}).then(done)
169+
})
170+
})
171+
172+
describe('places set, place not set on all children', () => {
173+
it('should be warned', done => {
174+
const spy = sinon.spy(console, 'warn')
175+
const el = document.createElement('div')
176+
const vm = new Vue({
177+
i18n,
178+
components,
179+
render (h) {
180+
return h('i18n', { props: { path: 'mixed', places: { 'x': 'foo' } } }, [
181+
h('comp', { props: { msg: 'bar' } })
182+
])
183+
}
184+
}).$mount(el)
185+
nextTick(() => {
186+
assert.equal(vm.$el.innerHTML, 'text: foo, component: ')
187+
assert(spy.notCalled === false)
188+
assert(spy.callCount === 1)
189+
spy.restore()
190+
}).then(done)
191+
})
192+
})
193+
100194
describe('fallback', () => {
101195
it('should be interpolated', done => {
102196
const el = document.createElement('div')

0 commit comments

Comments
 (0)