From e92b51d2c2794b3172a12e0ea2da9dcd25bc2d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szijj=C3=A1rt=C3=B3=20Nagy=20Misu?= Date: Fri, 18 Jan 2019 08:35:41 +0100 Subject: [PATCH 1/5] :zap: improvement(option): silentFallbackWarn (#139) * feature(option): add silentFallbackWarn to VueI18n constructor * silence fallback warnings * warn only if no translation is found at all --- src/index.js | 12 +++- src/mixin.js | 1 + test/unit/silent.test.js | 126 ++++++++++++++++++++++++++++++++++----- 3 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/index.js b/src/index.js index 570a92447..6b29a08dc 100644 --- a/src/index.js +++ b/src/index.js @@ -89,6 +89,9 @@ export default class VueI18n { this._silentTranslationWarn = options.silentTranslationWarn === undefined ? false : !!options.silentTranslationWarn + this._silentFallbackWarn = options.silentFallbackWarn === undefined + ? false + : !!options.silentFallbackWarn this._dateTimeFormatters = {} this._numberFormatters = {} this._path = new I18nPath() @@ -184,6 +187,9 @@ export default class VueI18n { get silentTranslationWarn (): boolean { return this._silentTranslationWarn } set silentTranslationWarn (silent: boolean): void { this._silentTranslationWarn = silent } + get silentFallbackWarn (): boolean { return this._silentFallbackWarn } + set silentFallbackWarn (silent: boolean): void { this._silentFallbackWarn = silent } + _getMessages (): LocaleMessages { return this._vm.messages } _getDateTimeFormats (): DateTimeFormats { return this._vm.dateTimeFormats } _getNumberFormats (): NumberFormats { return this._vm.numberFormats } @@ -230,7 +236,7 @@ export default class VueI18n { if (isPlainObject(message)) { ret = message[key] if (typeof ret !== 'string') { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !(this._silentFallbackWarn && (this._isFallbackRoot() || locale !== this.fallbackLocale))) { warn(`Value of key '${key}' is not a string!`) } return null @@ -359,7 +365,7 @@ export default class VueI18n { res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args, [key]) if (!isNull(res)) { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !this._silentFallbackWarn) { warn(`Fall back to translate the keypath '${key}' with '${fallback}' locale.`) } return res @@ -379,7 +385,7 @@ export default class VueI18n { host, 'string', parsedArgs.params ) if (this._isFallbackRoot(ret)) { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !this._silentFallbackWarn) { warn(`Fall back to translate the keypath '${key}' with root locale.`) } /* istanbul ignore if */ diff --git a/src/mixin.js b/src/mixin.js index cbf2debf3..15dd130c1 100644 --- a/src/mixin.js +++ b/src/mixin.js @@ -37,6 +37,7 @@ export default { options.i18n.formatter = this.$root.$i18n.formatter options.i18n.fallbackLocale = this.$root.$i18n.fallbackLocale options.i18n.silentTranslationWarn = this.$root.$i18n.silentTranslationWarn + options.i18n.silentFallbackWarn = this.$root.$i18n.silentFallbackWarn options.i18n.pluralizationRules = this.$root.$i18n.pluralizationRules options.i18n.preserveDirectiveContent = this.$root.$i18n.preserveDirectiveContent } diff --git a/test/unit/silent.test.js b/test/unit/silent.test.js index 33d46e6d0..b111c4992 100644 --- a/test/unit/silent.test.js +++ b/test/unit/silent.test.js @@ -1,25 +1,121 @@ describe('silent', () => { - it('should be suppressed translate warnings', () => { - const vm = new Vue({ - i18n: new VueI18n({ - locale: 'en', - silentTranslationWarn: true, + let spy + beforeEach(() => { + spy = sinon.spy(console, 'warn') + }) + afterEach(() => { + spy.restore() + }) + + describe('silentTranslationWarn', () => { + it('should be suppressed translate warnings', () => { + const vm = new Vue({ + i18n: new VueI18n({ + locale: 'en', + silentTranslationWarn: true, + messages: { + en: { who: 'root' }, + ja: { who: 'ルート' } + } + }) + }) + + vm.$t('foo.bar.buz') + assert(spy.notCalled === true) + + // change + vm.$i18n.silentTranslationWarn = false + vm.$t('foo.bar.buz') + assert(spy.callCount === 2) + }) + }) + + describe('silentFallbackWarn', () => { + let i18n + beforeEach(() => { + i18n = new VueI18n({ + locale: 'hu', + fallbackLocale: 'en', + silentFallbackWarn: true, messages: { - en: { who: 'root' }, - ja: { who: 'ルート' } + en: { winner: 'winner' }, + hu: { chickenDinner: 'csirkevacsora' } } }) }) - const spy = sinon.spy(console, 'warn') - vm.$t('foo.bar.buz') - assert(spy.notCalled === true) + it('should suppress `Fall back to ${fallback} locale` warnings', () => { + const vm = new Vue({ i18n }) + const warningRegex = /Fall back to .* 'en' locale./ + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) - // change - vm.$i18n.silentTranslationWarn = false - vm.$t('foo.bar.buz') - assert(spy.callCount === 2) + vm.$i18n.silentFallbackWarn = false + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) - spy.restore() + it('should suppress `Fall back to root locale` warnings.', () => { + const el = document.createElement('div') + const root = new Vue({ + i18n, + components: { + subComponent: { + i18n: { messages: { hu: { name: 'Név' } } }, + render (h) { return h('p') } + } + }, + render (h) { return h('sub-component') } + }).$mount(el) + const vm = root.$children[0] + const warningRegex = /Fall back to .* root locale./ + + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + + vm.$i18n.silentFallbackWarn = false + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) + + it('should suppress `not a string` warnings for fallback to fallbackLocale.', () => { + const vm = new Vue({ i18n }) + const warningRegex = /Value of .* is not a string./ + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + + vm.$i18n.silentFallbackWarn = false + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) + + it('should supress `not a string` warnings for fallback to root.', () => { + const el = document.createElement('div') + const root = new Vue({ + i18n, + components: { + subComponent: { + i18n: { messages: { hu: { name: 'Név' } } }, + render (h) { return h('p') } + } + }, + render (h) { return h('sub-component') } + }).$mount(el) + const vm = root.$children[0] + const warningRegex = /Value of .* is not a string./ + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + + vm.$i18n.silentFallbackWarn = false + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) + + it('should not suppress `not a string` warnings when no further fallback is possible.', () => { + const vm = new Vue({ i18n }) + const warningRegex = /Value of .* is not a string./ + vm.$t('loser') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) }) }) From db26d2731a92bcc4843f7e0b5648d9cc200d9642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szijj=C3=A1rt=C3=B3=20Nagy=20Misu?= Date: Fri, 18 Jan 2019 08:48:41 +0100 Subject: [PATCH 2/5] adding typescript property declaration --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 6b29a08dc..2397a6e81 100644 --- a/src/index.js +++ b/src/index.js @@ -54,6 +54,7 @@ export default class VueI18n { _watcher: any _i18nWatcher: Function _silentTranslationWarn: boolean + _silentFallbackWarn: boolean _dateTimeFormatters: Object _numberFormatters: Object _path: I18nPath From 2dbd95f7326e4507a5fb583b860ba27b2bc49985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szijj=C3=A1rt=C3=B3=20Nagy=20Misu?= Date: Mon, 21 Jan 2019 16:37:22 +0100 Subject: [PATCH 3/5] :pencil: docs(options): document silentFallbackWarn --- vuepress/api/README.md | 11 +++++++++++ vuepress/guide/component.md | 9 +++++++++ vuepress/guide/fallback.md | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/vuepress/api/README.md b/vuepress/api/README.md index 72fd7143e..f931d2fae 100644 --- a/vuepress/api/README.md +++ b/vuepress/api/README.md @@ -259,6 +259,17 @@ Whether suppress warnings outputted when localization fails. If `true`, suppress localization fail warnings. +#### silentFallbackWarn + +> 8.8+ + + * **Type:** `Boolean` + * **Default:** `false` + +Whether suppress warnings when falling back to either `fallbackLocale` or `root`. + +If `true`, warnings will be generated only when no translation is available at all, and not for fallbacks. + #### preserveDirectiveContent > 8.7+ diff --git a/vuepress/guide/component.md b/vuepress/guide/component.md index c5f83e383..3505a76f7 100644 --- a/vuepress/guide/component.md +++ b/vuepress/guide/component.md @@ -73,6 +73,15 @@ Outputs the following: As in the example above, if the component doesn't have the locale message, it falls back to globally defined localization info. The component uses the language set in the root instance (in the above example: `locale: 'ja'`). +Note, that by default falling back to root locale generates two warnings in the console: + +```console +[vue-i18n] Value of key 'message.greeting' is not a string! +[vue-i18n] Fall back to translate the keypath 'message.greeting' with root locale. +``` + +To suppress these warnings (while keeping those which warn of the total absence of translation for the given key) set `silentFallbackWarn: true` when initializing the `VueI18n` instance. + If you hope localize in the component locale, you can realize with `sync: false` and `locale` in `i18n` option. ## Translation in functional component diff --git a/vuepress/guide/fallback.md b/vuepress/guide/fallback.md index faa694d60..d618d3a2d 100644 --- a/vuepress/guide/fallback.md +++ b/vuepress/guide/fallback.md @@ -33,3 +33,12 @@ Output the below: ```html

hello world

``` + +Note, that by default falling back to `fallbackLocale` generates two console warnings: + +```console +[vue-i18n] Value of key 'message' is not a string! +[vue-i18n] Fall back to translate the keypath 'message' with 'en' locale. +``` + +To suppress these warnings (while keeping those which warn of the total absence of translation for the given key) set `silentFallbackWarn: true` when initializing the `VueI18n` instance. From 87649258b45b4f54ddf9015bef6c5fe94c4e516c Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Tue, 22 Jan 2019 10:34:12 +0100 Subject: [PATCH 4/5] Update vuepress/api/README.md Co-Authored-By: SzNagyMisu --- vuepress/api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vuepress/api/README.md b/vuepress/api/README.md index f931d2fae..fc8820987 100644 --- a/vuepress/api/README.md +++ b/vuepress/api/README.md @@ -261,7 +261,7 @@ If `true`, suppress localization fail warnings. #### silentFallbackWarn -> 8.8+ +> :new: 8.8+ * **Type:** `Boolean` * **Default:** `false` From 9a9da762fa2e85cba38b6d1acd46347883e5d5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szijj=C3=A1rt=C3=B3-Nagy=20Misu?= Date: Tue, 22 Jan 2019 11:11:37 +0100 Subject: [PATCH 5/5] :zap: improvement(option): silentFallbackWarn * include case when pathRet is not null, undefined, array, plain object or string * provide test case --- src/index.js | 8 +++- test/unit/silent.test.js | 101 ++++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/index.js b/src/index.js index 2397a6e81..42c24bcfe 100644 --- a/src/index.js +++ b/src/index.js @@ -217,6 +217,10 @@ export default class VueI18n { return !val && !isNull(this._root) && this._fallbackRoot } + _isSilentFallback (locale: Locale): boolean { + return this._silentFallbackWarn && (this._isFallbackRoot() || locale !== this.fallbackLocale) + } + _interpolate ( locale: Locale, message: LocaleMessageObject, @@ -237,7 +241,7 @@ export default class VueI18n { if (isPlainObject(message)) { ret = message[key] if (typeof ret !== 'string') { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !(this._silentFallbackWarn && (this._isFallbackRoot() || locale !== this.fallbackLocale))) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !this._isSilentFallback(locale)) { warn(`Value of key '${key}' is not a string!`) } return null @@ -250,7 +254,7 @@ export default class VueI18n { if (typeof pathRet === 'string') { ret = pathRet } else { - if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !this._isSilentFallback(locale)) { warn(`Value of key '${key}' is not a string!`) } return null diff --git a/test/unit/silent.test.js b/test/unit/silent.test.js index b111c4992..27801b9e0 100644 --- a/test/unit/silent.test.js +++ b/test/unit/silent.test.js @@ -78,37 +78,84 @@ describe('silent', () => { assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) }) - it('should suppress `not a string` warnings for fallback to fallbackLocale.', () => { - const vm = new Vue({ i18n }) - const warningRegex = /Value of .* is not a string./ - vm.$t('winner') - assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + describe('if first try is null or undefined,', () => { + it('should suppress `not a string` warnings for fallback to fallbackLocale.', () => { + const vm = new Vue({ i18n }) + const warningRegex = /Value of .* is not a string./ + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) - vm.$i18n.silentFallbackWarn = false - vm.$t('winner') - assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + vm.$i18n.silentFallbackWarn = false + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) + + it('should supress `not a string` warnings for fallback to root.', () => { + const el = document.createElement('div') + const root = new Vue({ + i18n, + components: { + subComponent: { + i18n: { messages: { hu: { name: 'Név' } } }, + render (h) { return h('p') } + } + }, + render (h) { return h('sub-component') } + }).$mount(el) + const vm = root.$children[0] + const warningRegex = /Value of .* is not a string./ + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + + vm.$i18n.silentFallbackWarn = false + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) }) - it('should supress `not a string` warnings for fallback to root.', () => { - const el = document.createElement('div') - const root = new Vue({ - i18n, - components: { - subComponent: { - i18n: { messages: { hu: { name: 'Név' } } }, - render (h) { return h('p') } - } - }, - render (h) { return h('sub-component') } - }).$mount(el) - const vm = root.$children[0] - const warningRegex = /Value of .* is not a string./ - vm.$t('chickenDinner') - assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + describe('if first try is not null, undefined, array, plain object or string,', () => { + it('should suppress `not a string` warnings for fallback to fallbackLocale.', () => { + const vm = new Vue({ + i18n: new VueI18n({ + locale: 'hu', + fallbackLocale: 'en', + silentFallbackWarn: true, + messages: { + en: { winner: 'winner' }, + hu: { winner: true } // translation value is boolean + } + }) + }) + const warningRegex = /Value of .* is not a string./ + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) - vm.$i18n.silentFallbackWarn = false - vm.$t('chickenDinner') - assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + vm.$i18n.silentFallbackWarn = false + vm.$t('winner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) + + it('should supress `not a string` warnings for fallback to root.', () => { + const el = document.createElement('div') + const root = new Vue({ + i18n, + components: { + subComponent: { + i18n: { messages: { hu: { chickenDinner: 11 } } }, // translation value is number + render (h) { return h('p') } + } + }, + render (h) { return h('sub-component') } + }).$mount(el) + const vm = root.$children[0] + const warningRegex = /Value of .* is not a string./ + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === false) + + vm.$i18n.silentFallbackWarn = false + vm.$t('chickenDinner') + assert(spy.getCalls().some(call => call.args[0].match(warningRegex)) === true) + }) }) it('should not suppress `not a string` warnings when no further fallback is possible.', () => {