diff --git a/src/extend.js b/src/extend.js index 6aa95661a..258af7ac7 100644 --- a/src/extend.js +++ b/src/extend.js @@ -20,4 +20,8 @@ export default function extend (Vue: any): void { const i18n = this.$i18n return i18n.d(value, ...args) } + + Vue.prototype.$n = function (value: number, ...args: any): NumberFormatResult { + return this.$i18n.n(value, ...args) + } } diff --git a/src/index.js b/src/index.js index 12a4b2ef5..10a4c3597 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,8 @@ import { isPlainObject, isObject, looseClone, - canUseDateTimeFormat + canUseDateTimeFormat, + canUseNumberFormat } from './util' import BaseFormatter from './format' import getPathValue from './path' @@ -32,12 +33,14 @@ export default class VueI18n { _i18nWatcher: Function _silentTranslationWarn: boolean _dateTimeFormatters: Object + _numberFormatters: Object constructor (options: I18nOptions = {}) { const locale: Locale = options.locale || 'en-US' const fallbackLocale: Locale = options.fallbackLocale || 'en-US' const messages: LocaleMessages = options.messages || {} const dateTimeFormats = options.dateTimeFormats || {} + const numberFormats = options.numberFormats || {} this._vm = null this._formatter = options.formatter || new BaseFormatter() this._missing = options.missing || null @@ -50,20 +53,28 @@ export default class VueI18n { ? false : !!options.silentTranslationWarn this._dateTimeFormatters = {} + this._numberFormatters = {} this._exist = (message: Object, key: Path): boolean => { if (!message || !key) { return false } return !isNull(getPathValue(message, key)) } - this._initVM({ locale, fallbackLocale, messages, dateTimeFormats }) + this._initVM({ + locale, + fallbackLocale, + messages, + dateTimeFormats, + numberFormats + }) } _initVM (data: { locale: Locale, fallbackLocale: Locale, messages: LocaleMessages, - dateTimeFormats: DateTimeFormats + dateTimeFormats: DateTimeFormats, + numberFormats: NumberFormats }): void { const silent = Vue.config.silent Vue.config.silent = true @@ -109,6 +120,7 @@ export default class VueI18n { get messages (): LocaleMessages { return looseClone(this._vm.messages) } get dateTimeFormats (): DateTimeFormats { return looseClone(this._vm.dateTimeFormats) } + get numberFormats (): NumberFormats { return looseClone(this._vm.numberFormats) } get locale (): Locale { return this._vm.locale } set locale (locale: Locale): void { @@ -342,10 +354,80 @@ export default class VueI18n { return this._d(value, locale, key) } + + getNumberFormat (locale: Locale): NumberFormat { + return looseClone(this._vm.numberFormats[locale]) + } + + setNumberFormat (locale: Locale, format: NumberFormat): void { + this._vm.numberFormats[locale] = format + } + + mergeNumberFormat (locale: Locale, format: NumberFormat): void { + this._vm.numberFormats[locale] = Vue.util.extend(this.getNumberFormat(locale), format) + } + + _n (value: number, _locale: Locale, key: ?string): NumberFormatResult { + if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.numberFormat) { + warn('Cannot format a Date value due to not support Intl.NumberFormat.') + return '' + } + + let ret = '' + const numberFormats = this.numberFormats + if (key) { + let locale: Locale = _locale + if (isNull(numberFormats[_locale][key])) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { + warn(`Fall back to the numberFormat of key '${key}' with '${this.fallbackLocale}' locale.`) + } + locale = this.fallbackLocale + } + const id = `${locale}__${key}` + let formatter = this._numberFormatters[id] + const format = numberFormats[locale][key] + if (!formatter) { + formatter = this._numberFormatters[id] = Intl.NumberFormat(locale, format) + } + ret = formatter.format(value) + } else { + ret = Intl.NumberFormat(_locale).format(value) + } + + return ret + } + + n (value: number, ...args: any): NumberFormatResult { + let locale: Locale = this.locale + let key: ?string = null + + if (args.length === 1) { + if (typeof args[0] === 'string') { + key = args[0] + } else if (isObject(args[0])) { + if (args[0].locale) { + locale = args[0].locale + } + if (args[0].key) { + key = args[0].key + } + } + } else if (args.length === 2) { + if (typeof args[0] === 'string') { + key = args[0] + } + if (typeof args[1] === 'string') { + locale = args[1] + } + } + + return this._n(value, locale, key) + } } VueI18n.availabilities = { - dateTimeFormat: canUseDateTimeFormat + dateTimeFormat: canUseDateTimeFormat, + numberFormat: canUseNumberFormat } VueI18n.install = install VueI18n.version = '__VERSION__' diff --git a/src/util.js b/src/util.js index f53703c64..23c84a685 100644 --- a/src/util.js +++ b/src/util.js @@ -98,3 +98,6 @@ export function looseClone (obj: Object): Object { export const canUseDateTimeFormat: boolean = typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined' + +export const canUseNumberFormat: boolean = + typeof Intl !== 'undefined' && typeof Intl.NumberFormat !== 'undefined' diff --git a/test/unit/basic.test.js b/test/unit/basic.test.js index d21f50cb2..971e09bc3 100644 --- a/test/unit/basic.test.js +++ b/test/unit/basic.test.js @@ -1,5 +1,6 @@ import messages from './fixture/index' import dateTimeFormats from './fixture/datetime' +import numberFormats from './fixture/number' describe('basic', () => { let i18n @@ -627,4 +628,48 @@ describe('basic', () => { }) }) }) + + desc('i18n#n', () => { + let money + beforeEach(() => { + i18n = new VueI18n({ + locale: 'en-US', + fallbackLocale: 'ja-JP', + numberFormats + }) + money = 10100 + }) + + describe('arguments nothing', () => { + it('should be formatted', () => { + assert.equal(i18n.n(money), '10,100') + }) + }) + + describe('key argument', () => { + it('should be formatted', () => { + assert.equal(i18n.n(money, 'currency'), '$10,100.00') + }) + }) + + describe('locale argument', () => { + describe('with second argument', () => { + it('should be formatted', () => { + assert.equal(i18n.n(money, 'currency', 'ja-JP'), '¥10,100') + }) + }) + + describe('with object argument', () => { + it('should be formatted', () => { + assert.equal(i18n.n(money, { key: 'currency', locale: 'ja-JP' }), '¥10,100') + }) + }) + }) + + describe('fallback', () => { + it('should be formatted', () => { + assert.equal(i18n.n(0.9, 'percent'), '90%') + }) + }) + }) }) diff --git a/test/unit/fixture/number.js b/test/unit/fixture/number.js new file mode 100644 index 000000000..51211da15 --- /dev/null +++ b/test/unit/fixture/number.js @@ -0,0 +1,21 @@ +export default { + 'en-US': { + currency: { + style: 'currency', currency: 'USD', currencyDisplay: 'symbol' + }, + decimal: { + style: 'decimal', useGrouping: false + } + }, + 'ja-JP': { + currency: { + style: 'currency', currency: 'JPY', currencyDisplay: 'symbol' + }, + numeric: { + style: 'decimal', useGrouping: false + }, + percent: { + style: 'percent', useGrouping: false + } + } +} diff --git a/test/unit/number.test.js b/test/unit/number.test.js new file mode 100644 index 000000000..885ba0b8f --- /dev/null +++ b/test/unit/number.test.js @@ -0,0 +1,50 @@ +import numberFormats from './fixture/number' + +const desc = VueI18n.availabilities.numberFormat ? describe : describe.skip +desc('number format', () => { + describe('getNumberFormat / setNumberFormat', () => { + it('should be worked', done => { + const i18n = new VueI18n({ + locale: 'en-US', + numberFormats + }) + const el = document.createElement('div') + document.body.appendChild(el) + + const money = 101 + const vm = new Vue({ + i18n, + render (h) { + return h('p', { ref: 'text' }, [this.$n(money, 'currency')]) + } + }).$mount(el) + + const { text } = vm.$refs + const zhFormat = { + currency: { + style: 'currency', currency: 'CNY', currencyDisplay: 'name' + } + } + nextTick(() => { + assert.equal(text.textContent, '$101.00') + i18n.setNumberFormat('zh-CN', zhFormat) + assert.deepEqual(i18n.getNumberFormat('zh-CN'), zhFormat) + i18n.locale = 'zh-CN' + }).then(() => { + assert.equal(text.textContent, '101.00人民币') + }).then(done) + }) + }) + + describe('mergeNumberFormat', () => { + it('should be merged', () => { + const i18n = new VueI18n({ + locale: 'ja-JP', + numberFormats + }) + const percent = { style: 'percent' } + i18n.mergeNumberFormat('en-US', { percent }) + assert.deepEqual(percent, i18n.getNumberFormat('en-US').percent) + }) + }) +})