From be9e1bde7b34e21b948e2817e919bb79a125722a Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sat, 3 Jun 2017 00:05:04 +0900 Subject: [PATCH] :bug: bug: fix datetime and number fallback localization Closes #168 --- src/index.js | 132 +++++++++++++++++++++++++----------- test/unit/component.test.js | 59 +++++++++++----- 2 files changed, 133 insertions(+), 58 deletions(-) diff --git a/src/index.js b/src/index.js index e8bc7adaf..934d26692 100644 --- a/src/index.js +++ b/src/index.js @@ -346,35 +346,61 @@ export default class VueI18n { this._vm.dateTimeFormats[locale] = Vue.util.extend(this.getDateTimeFormat(locale), format) } - _d (value: number | Date, _locale: Locale, key: ?string): DateTimeFormatResult { + _localizeDateTime ( + value: number | Date, + locale: Locale, + fallback: Locale, + dateTimeFormats: DateTimeFormats, + key: ?string + ): ?DateTimeFormatResult { + let _locale: Locale = locale + let formats: DateTimeFormat = dateTimeFormats[_locale] + + // fallback locale + if (isNull(formats) || isNull(formats[key])) { + if (process.env.NODE_ENV !== 'production') { + warn(`Fall back to '${fallback}' datetime formats from '${locale} datetime formats.`) + } + _locale = fallback + formats = dateTimeFormats[_locale] + } + + if (isNull(formats) || isNull(formats[key])) { + return null + } else { + const format: ?DateTimeFormatOptions = formats[key] + const id = `${_locale}__${key}` + let formatter = this._dateTimeFormatters[id] + if (!formatter) { + formatter = this._dateTimeFormatters[id] = new Intl.DateTimeFormat(_locale, format) + } + return formatter.format(value) + } + } + + _d (value: number | Date, locale: Locale, key: ?string): DateTimeFormatResult { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !VueI18n.availabilities.dateTimeFormat) { warn('Cannot format a Date value due to not support Intl.DateTimeFormat.') return '' } - let ret = '' - const dateTimeFormats = this._getDateTimeFormats() - if (key) { - let locale: Locale = _locale - if (isNull(dateTimeFormats[_locale][key])) { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { - warn(`Fall back to the dateTimeFormat of key '${key}' with '${this.fallbackLocale}' locale.`) - } - locale = this.fallbackLocale - } - const id = `${locale}__${key}` - let formatter = this._dateTimeFormatters[id] - const format = dateTimeFormats[locale][key] - if (!formatter) { - formatter = this._dateTimeFormatters[id] = Intl.DateTimeFormat(locale, format) + if (!key) { + return new Intl.DateTimeFormat(locale).format(value) + } + + const ret: ?DateTimeFormatResult = + this._localizeDateTime(value, locale, this.fallbackLocale, this._getDateTimeFormats(), key) + if (this._isFallbackRoot(ret)) { + if (process.env.NODE_ENV !== 'production') { + warn(`Fall back to datetime localization of root: key '${key}' .`) } - ret = formatter.format(value) + /* istanbul ignore if */ + if (!this._root) { throw Error('unexpected error') } + return this._root.d(value, key, locale) } else { - ret = Intl.DateTimeFormat(_locale).format(value) + return ret } - - return ret } d (value: number | Date, ...args: any): DateTimeFormatResult { @@ -416,35 +442,61 @@ export default class VueI18n { this._vm.numberFormats[locale] = Vue.util.extend(this.getNumberFormat(locale), format) } - _n (value: number, _locale: Locale, key: ?string): NumberFormatResult { + _localizeNumber ( + value: number, + locale: Locale, + fallback: Locale, + numberFormats: NumberFormats, + key: string + ): ?NumberFormatResult { + let _locale: Locale = locale + let formats: NumberFormat = numberFormats[_locale] + + // fallback locale + if (isNull(formats) || isNull(formats[key])) { + if (process.env.NODE_ENV !== 'production') { + warn(`Fall back to '${fallback}' number formats from '${locale} number formats.`) + } + _locale = fallback + formats = numberFormats[_locale] + } + + if (isNull(formats) || isNull(formats[key])) { + return null + } else { + const format: ?NumberFormatOptions = formats[key] + const id = `${_locale}__${key}` + let formatter = this._numberFormatters[id] + if (!formatter) { + formatter = this._numberFormatters[id] = new Intl.NumberFormat(_locale, format) + } + return formatter.format(value) + } + } + + _n (value: number, locale: Locale, key: ?string): NumberFormatResult { /* istanbul ignore if */ 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._getNumberFormats() - 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) + if (!key) { + return new Intl.NumberFormat(locale).format(value) + } + + const ret: ?NumberFormatResult = + this._localizeNumber(value, locale, this.fallbackLocale, this._getNumberFormats(), key) + if (this._isFallbackRoot(ret)) { + if (process.env.NODE_ENV !== 'production') { + warn(`Fall back to number localization of root: key '${key}' .`) } - ret = formatter.format(value) + /* istanbul ignore if */ + if (!this._root) { throw Error('unexpected error') } + return this._root.n(value, key, locale) } else { - ret = Intl.NumberFormat(_locale).format(value) + return ret } - - return ret } n (value: number, ...args: any): NumberFormatResult { diff --git a/test/unit/component.test.js b/test/unit/component.test.js index 71be0f944..3409e1df4 100644 --- a/test/unit/component.test.js +++ b/test/unit/component.test.js @@ -1,18 +1,27 @@ +import dateTimeFormats from './fixture/datetime' +import numberFormats from './fixture/number' + describe('component translation', () => { let vm, i18n + const dt = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)) + const money = 101 + const messages = { + 'en-US': { + who: 'root', + fallback: 'fallback' + }, + 'ja-JP': { + who: 'ルート', + fallback: 'フォールバック' + } + } + beforeEach(done => { i18n = new VueI18n({ - locale: 'ja', - messages: { - en: { - who: 'root', - fallback: 'fallback' - }, - ja: { - who: 'ルート', - fallback: 'フォールバック' - } - } + locale: 'ja-JP', + messages, + dateTimeFormats, + numberFormats }) const el = document.createElement('div') @@ -21,11 +30,11 @@ describe('component translation', () => { components: { child1: { // translation with component i18n: { - locale: 'en', + locale: 'en-US', sync: false, messages: { - en: { who: 'child1' }, - ja: { who: '子1' } + 'en-US': { who: 'child1' }, + 'ja-JP': { who: '子1' } } }, components: { @@ -41,6 +50,8 @@ describe('component translation', () => { return h('div', {}, [ h('p', { ref: 'who' }, [this.$t('who')]), h('p', { ref: 'fallback' }, [this.$t('fallback')]), + h('p', { ref: 'datetime' }, [this.$d(dt, 'short')]), + h('p', { ref: 'number' }, [this.$n(money, 'currency')]), h('sub-child1', { ref: 'sub-child1' }) ]) } @@ -50,8 +61,8 @@ describe('component translation', () => { 'sub-child2': { i18n: { messages: { - en: { who: 'sub-child2' }, - ja: { who: 'サブの子2' } + 'en-US': { who: 'sub-child2' }, + 'ja-JP': { who: 'サブの子2' } } }, render (h) { @@ -84,23 +95,35 @@ describe('component translation', () => { const root = vm.$refs.who const child1 = vm.$refs.child1.$refs.who const child1Fallback = vm.$refs.child1.$refs.fallback + const child1DateTime = vm.$refs.child1.$refs.datetime + const child1Number = vm.$refs.child1.$refs.number const child2 = vm.$refs.child2.$refs.who const subChild1 = vm.$refs.child1.$refs['sub-child1'].$refs.who const subChild2 = vm.$refs.child2.$refs['sub-child2'].$refs.who assert.equal(root.textContent, 'ルート') assert.equal(child1.textContent, 'child1') assert.equal(child1Fallback.textContent, 'フォールバック') + assert.equal( + child1DateTime.textContent, + isWebkit ? '12/20/2012, 03:00' : '12/19/2012, 10:00 PM' + ) + assert.equal(child1Number.textContent, '$101.00') assert.equal(child2.textContent, 'ルート') assert.equal(subChild1.textContent, 'ルート') assert.equal(subChild2.textContent, 'サブの子2') // change locale - i18n.locale = 'en' - vm.$refs.child1.$i18n.locale = 'ja' + i18n.locale = 'en-US' + vm.$refs.child1.$i18n.locale = 'ja-JP' nextTick(() => { assert.equal(root.textContent, 'root') assert.equal(child1.textContent, '子1') assert.equal(child1Fallback.textContent, 'fallback') + assert.equal( + child1DateTime.textContent, + isWebkit ? '2012/12/20 3:00' : '2012/12/20 12:00' + ) + assert.equal(child1Number.textContent, '¥101') assert.equal(child2.textContent, 'root') assert.equal(subChild1.textContent, 'root') assert.equal(subChild2.textContent, 'sub-child2')