diff --git a/src/directive.js b/src/directive.js new file mode 100644 index 000000000..5e9dbae49 --- /dev/null +++ b/src/directive.js @@ -0,0 +1,68 @@ +/* @flow */ + +import { warn, isPlainObject, looseEqual } from './util' + +export function bind (el: any, binding: Object, vnode: any): void { + t(el, binding, vnode) +} + +export function update (el: any, binding: Object, vnode: any, oldVNode: any): void { + if (looseEqual(binding.value, binding.oldValue)) { return } + + t(el, binding, vnode) +} + +function t (el: any, binding: Object, vnode: any): void { + const value: any = binding.value + + const { path, locale, args } = parseValue(value) + if (!path && !locale && !args) { + warn('not support value type') + return + } + + const vm: any = vnode.context + if (!vm) { + warn('not exist Vue instance in VNode context') + return + } + + if (!vm.$i18n) { + warn('not exist VueI18n instance in Vue instance') + return + } + + if (!path) { + warn('required `path` in v-t directive') + return + } + + el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args)) +} + +function parseValue (value: any): Object { + let path: ?string + let locale: ?Locale + let args: any + + if (typeof value === 'string') { + path = value + } else if (isPlainObject(value)) { + path = value.path + locale = value.locale + args = value.args + } + + return { path, locale, args } +} + +function makeParams (locale: Locale, args: any): Array { + const params: Array = [] + + locale && params.push(locale) + if (args && (Array.isArray(args) || isPlainObject(args))) { + params.push(args) + } + + return params +} diff --git a/src/install.js b/src/install.js index 6707d2f5a..6ac4e473b 100644 --- a/src/install.js +++ b/src/install.js @@ -2,6 +2,7 @@ import { warn } from './util' import extend from './extend' import mixin from './mixin' import component from './component' +import { bind, update } from './directive' export let Vue @@ -28,6 +29,7 @@ export function install (_Vue) { extend(Vue) Vue.mixin(mixin) + Vue.directive('t', { bind, update }) Vue.component(component.name, component) // use object-based merge strategy diff --git a/src/util.js b/src/util.js index c0d59eb74..b661472a6 100644 --- a/src/util.js +++ b/src/util.js @@ -14,7 +14,7 @@ export function warn (msg: string, err: ?Error): void { } } -export function isObject (obj: mixed): boolean { +export function isObject (obj: mixed): boolean %checks { return obj !== null && typeof obj === 'object' } @@ -114,6 +114,39 @@ export function merge (target: Object): Object { return output } +export function looseEqual (a: any, b: any): boolean { + if (a === b) { return true } + const isObjectA: boolean = isObject(a) + const isObjectB: boolean = isObject(b) + if (isObjectA && isObjectB) { + try { + const isArrayA: boolean = Array.isArray(a) + const isArrayB: boolean = Array.isArray(b) + if (isArrayA && isArrayB) { + return a.length === b.length && a.every((e: any, i: number): boolean => { + return looseEqual(e, b[i]) + }) + } else if (!isArrayA && !isArrayB) { + const keysA: Array = Object.keys(a) + const keysB: Array = Object.keys(b) + return keysA.length === keysB.length && keysA.every((key: string): boolean => { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + export const canUseDateTimeFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined' diff --git a/test/unit/directive.test.js b/test/unit/directive.test.js new file mode 100644 index 000000000..b1bc07210 --- /dev/null +++ b/test/unit/directive.test.js @@ -0,0 +1,120 @@ +import messages from './fixture/index' + +describe('custom directive', () => { + let i18n + beforeEach(() => { + i18n = new VueI18n({ + locale: 'en', + messages + }) + }) + + function createVM (options) { + const el = document.createElement('div') + return new Vue(options).$mount(el) + } + + describe('v-t', () => { + describe('string literal', () => { + it('should be translated', done => { + const vm = createVM({ + i18n, + render (h) { + //

+ return h('p', { ref: 'text', directives: [{ + name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'" + }] }) + } + }) + nextTick(() => { + assert.equal(vm.$refs.text.textContent, messages.en.message.hello) + assert.equal(vm.$refs.text._vt, messages.en.message.hello) + }).then(done) + }) + }) + + describe('object', () => { + it('should be translated', done => { + const vm = createVM({ + i18n, + data: { + msgPath: 'message.format.named' + }, + render (h) { + //

+ return h('p', { ref: 'text', directives: [{ + name: 't', rawName: 'v-t', + value: ({ path: this.msgPath, locale: 'ja', args: { name: 'kazupon' } }), + expression: "{ path: msgPath, locale: 'ja', args: { name: 'kazupon' } }" + }] }) + } + }) + nextTick(() => { + const expected = 'こんにちは kazupon, ごきげんいかが?' + assert.equal(vm.$refs.text.textContent, expected) + assert.equal(vm.$refs.text._vt, expected) + }).then(done) + }) + }) + + describe('not support warning', () => { + it('should be warned', done => { + const spy = sinon.spy(console, 'warn') + createVM({ + i18n, + render (h) { + //

+ return h('p', { ref: 'text', directives: [{ + name: 't', rawName: 'v-t', value: ([1]), expression: '[1]' + }] }) + } + }) + nextTick(() => { + assert(spy.notCalled === false) + assert(spy.callCount === 1) + spy.restore() + }).then(done) + }) + }) + + describe('path required warning', () => { + it('should be warned', done => { + const spy = sinon.spy(console, 'warn') + createVM({ + i18n, + render (h) { + //

+ return h('p', { ref: 'text', directives: [{ + name: 't', rawName: 'v-t', + value: ({ locale: 'ja', args: { name: 'kazupon' } }), + expression: "{ locale: 'ja', args: { name: 'kazupon' } }" + }] }) + } + }) + nextTick(() => { + assert(spy.notCalled === false) + assert(spy.callCount === 1) + spy.restore() + }).then(done) + }) + }) + + describe('VueI18n instance warning', () => { + it('should be warned', done => { + const spy = sinon.spy(console, 'warn') + createVM({ + render (h) { + return h('p', { ref: 'text', directives: [{ + name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'" + }] }) + } + }) + nextTick(() => { + assert(spy.notCalled === false) + assert(spy.callCount === 1) + spy.restore() + }).then(done) + }) + }) + }) +})