Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List formatting refactor and places/place feature #218

Merged
merged 5 commits into from
Aug 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions gitbook/en/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ Output the below:
<p>hello world</p>
```

List formatting also accepts array-like object:

```html
<p>{{ $t('message.hello', {'0': 'hello'}) }}</p>
```

Output the below:

```html
<p>hello world</p>
```

## Support ruby on rails i18n format

Locale messages the below:
Expand Down
79 changes: 78 additions & 1 deletion gitbook/en/interpolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,81 @@ About the above example, see the [example](https://github.com/kazupon/vue-i18n/t

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.

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.
In above example, the component interpolation follows the **list formatting**. The children of `i18n` functional component are interpolated by their orders of appearance.

> :warning: NOTE: In `i18n` component, text content consists of only white spaces will be omitted.

Named formatting is supported with the help of `place` attribute. For example:

```html
<div id="app">
<!-- ... -->
<i18n path="info" tag="p">
<span place="limit">{{ changeLimit }}</span>
<a place="action" :href="changeUrl">{{ $t('change') }}</a>
</i18n>
<!-- ... -->
</div>
```

```javascript
const messages = {
en: {
info: 'You can {action} until {limit} minutes from departure.',
change: 'change your flight',
refund: 'refund the ticket'
}
}

const i18n = new VueI18n({
locale: 'en',
messages
})
new Vue({
i18n,
data: {
changeUrl: '/change',
refundUrl: '/refund',
changeLimit: 15,
refundLimit: 30
}
}).$mount('#app')
```

Outputs:

```html
<div id="app">
<!-- ... -->
<p>
You can <a href="/change">change your flight</a> until <span>15</span> minutes from departure.
</p>
<!-- ... -->
</div>
```

> :warning: NOTE: To use named formatting, all children of `i18n` component must have `place` attribute set. Otherwise it will fallback to list formatting.

If you still want to interpolate text content in named formatting, you could define `places` property on `i18n` component. For example:

```html
<div id="app">
<!-- ... -->
<i18n path="info" tag="p" :places="{ limit: refundLimit }">
<a place="action" :href="refundUrl">{{ $t('refund') }}</a>
</i18n>
<!-- ... -->
</div>
```

Outputs:

```html
<div id="app">
<!-- ... -->
<p>
You can <a href="/refund">refund your ticket</a> until 30 minutes from departure.
</p>
<!-- ... -->
</div>
```
47 changes: 41 additions & 6 deletions src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ export default {
},
locale: {
type: String
},
places: {
type: [Array, Object]
}
},
render (h: Function, { props, data, children, parent }: Object) {
const i18n = parent.$i18n

children = (children || []).filter(child => {
return child.tag || (child.text = child.text.trim())
})

if (!i18n) {
if (process.env.NODE_ENV !== 'production') {
warn('Cannot find VueI18n instance!')
Expand All @@ -30,14 +38,41 @@ export default {
const path: Path = props.path
const locale: ?Locale = props.locale

const params: Array<any> = []
locale && params.push(locale)
children.forEach(child => {
if (child.tag || child.text.trim()) {
params.push(child)
const params: Object = {}
const places: Array<any> | Object = props.places || {}

const hasPlaces: boolean = Array.isArray(places)
? places.length > 0
: Object.keys(places).length > 0

const everyPlace: boolean = children.every(child => {
if (child.data && child.data.attrs) {
const place = child.data.attrs.place
return (typeof place !== 'undefined') && place !== ''
}
})

return h(props.tag, data, i18n.i(path, ...params))
if (hasPlaces && children.length > 0 && !everyPlace) {
warn('If places prop is set, all child elements must have place prop set.')
}

if (Array.isArray(places)) {
places.forEach((el, i) => {
params[i] = el
})
} else {
Object.keys(places).forEach(key => {
params[key] = places[key]
})
}

children.forEach((child, i: number) => {
const key: string = everyPlace
? `${child.data.attrs.place}`
: `${i}`
params[key] = child
})

return h(props.tag, data, i18n.i(path, locale, params))
}
}
8 changes: 1 addition & 7 deletions src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,7 @@ export function compile (tokens: Array<Token>, values: Object | Array<any>): Arr
compiled.push(token.value)
break
case 'list':
if (mode === 'list') {
compiled.push(values[parseInt(token.value, 10)])
} else {
if (process.env.NODE_ENV !== 'production') {
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
}
}
compiled.push(values[parseInt(token.value, 10)])
break
case 'named':
if (mode === 'named') {
Expand Down
20 changes: 6 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,37 +321,29 @@ export default class VueI18n {
return this._t(key, this.locale, this._getMessages(), null, ...values)
}

_i (key: Path, locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
_i (key: Path, locale: Locale, messages: LocaleMessages, host: any, values: Object): any {
const ret: any =
this._translate(messages, locale, this.fallbackLocale, key, host, 'raw', values)
if (this._isFallbackRoot(ret)) {
if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) {
warn(`Fall back to interpolate the keypath '${key}' with root locale.`)
}
if (!this._root) { throw Error('unexpected error') }
return this._root.i(key, ...values)
return this._root.i(key, locale, values)
} else {
return this._warnDefault(locale, key, ret, host)
}
}

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

let locale: Locale = this.locale
let index: number = 0
if (typeof values[0] === 'string') {
locale = values[0]
index = 1
}

const params: Array<any> = []
for (let i = index; i < values.length; i++) {
params.push(values[i])
if (typeof locale !== 'string') {
locale = this.locale
}

return this._i(key, locale, this._getMessages(), null, ...params)
return this._i(key, locale, this._getMessages(), null, values)
}

_tc (
Expand Down
20 changes: 15 additions & 5 deletions test/unit/format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,27 @@ describe('compile', () => {
})
})

describe('list token with named value', () => {
it('should be compiled', () => {
const tokens = parse('name: {0}, age: {1}') // list tokens
const compiled = compile(tokens, { '0': 'kazupon', '1': '0x20' }) // named values
assert(compiled.length === 4)
assert.equal(compiled[0], 'name: ')
assert.equal(compiled[1], 'kazupon')
assert.equal(compiled[2], ', age: ')
assert.equal(compiled[3], '0x20')
})
})

describe('unmatch values mode', () => {
it('should be warned', () => {
const spy = sinon.spy(console, 'warn')

const tokens1 = parse('name: {0}, age: {1}') // list tokens
compile(tokens1, { name: 'kazupon', age: '0x20' }) // named values
const tokens2 = parse('name: {name}, age: {age}') // named tokens
compile(tokens2, ['kazupon', '0x20']) // list values
const tokens = parse('name: {name}, age: {age}') // named tokens
compile(tokens, ['kazupon', '0x20']) // list values

assert(spy.notCalled === false)
assert(spy.callCount === 4)
assert(spy.callCount === 2)
spy.restore()
})
})
Expand Down
102 changes: 98 additions & 4 deletions test/unit/interpolation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import Component from '../../src/component'
const messages = {
en: {
text: 'one: {0}',
premitive: 'one: {0}, two: {1}',
primitive: 'one: {0}, two: {1}',
component: 'element: {0}, component: {1}',
link: '@:premitive',
mixed: 'text: {x}, component: {y}',
link: '@:primitive',
term: 'I accept xxx {0}.',
tos: 'Term of service',
fallback: 'fallback from {0}'
Expand Down Expand Up @@ -60,13 +61,13 @@ describe('component interpolation', () => {
})
})

describe('premitive nodes', () => {
describe('primitive nodes', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'premitive' } }, [
return h('i18n', { props: { path: 'primitive' } }, [
h('p', ['1']),
h('p', ['2'])
])
Expand Down Expand Up @@ -97,6 +98,99 @@ describe('component interpolation', () => {
})
})

describe('places prop', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'text', places: [1] } })
}
}).$mount(el)
nextTick(() => {
assert.equal(vm.$el.textContent, 'one: 1')
}).then(done)
})
})

describe('place prop on all children', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
components,
render (h) {
return h('i18n', { props: { path: 'component' } }, [
h('p', { props: { place: 0 } }, ['1']),
h('comp', { props: { place: 1, msg: 'foo' } })
])
}
}).$mount(el)
nextTick(() => {
assert.equal(vm.$el.innerHTML, 'element: <p>1</p>, component: <p>foo</p>')
}).then(done)
})
})

describe('place prop on some children', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
components,
render (h) {
return h('i18n', { props: { path: 'component' } }, [
h('p', { props: { place: 1 } }, ['1']),
h('comp', { props: { msg: 'foo' } })
])
}
}).$mount(el)
nextTick(() => {
assert.equal(vm.$el.innerHTML, 'element: <p>1</p>, component: <p>foo</p>')
}).then(done)
})
})

describe('places and place mixed', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
components,
render (h) {
return h('i18n', { props: { path: 'mixed', places: { 'x': 'foo' } } }, [
h('comp', { props: { msg: 'bar' }, attrs: { place: 'y' } })
])
}
}).$mount(el)
nextTick(() => {
assert.equal(vm.$el.innerHTML, 'text: foo, component: <p place="y">bar</p>')
}).then(done)
})
})

describe('places set, place not set on all children', () => {
it('should be warned', done => {
const spy = sinon.spy(console, 'warn')
const el = document.createElement('div')
const vm = new Vue({
i18n,
components,
render (h) {
return h('i18n', { props: { path: 'mixed', places: { 'x': 'foo' } } }, [
h('comp', { props: { msg: 'bar' } })
])
}
}).$mount(el)
nextTick(() => {
assert.equal(vm.$el.innerHTML, 'text: foo, component: ')
assert(spy.notCalled === false)
assert(spy.callCount === 1)
spy.restore()
}).then(done)
})
})

describe('fallback', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
Expand Down