diff --git a/src/index.js b/src/index.js index 570a92447..42c24bcfe 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 @@ -89,6 +90,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 +188,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 } @@ -210,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, @@ -230,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) { + if (process.env.NODE_ENV !== 'production' && !this._silentTranslationWarn && !this._isSilentFallback(locale)) { warn(`Value of key '${key}' is not a string!`) } return null @@ -243,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 @@ -359,7 +370,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 +390,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..27801b9e0 100644 --- a/test/unit/silent.test.js +++ b/test/unit/silent.test.js @@ -1,25 +1,168 @@ 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) + }) + + 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) + }) + + 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) + }) + }) + + 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('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.', () => { + 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) + }) }) }) diff --git a/vuepress/api/README.md b/vuepress/api/README.md index 72fd7143e..fc8820987 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 + +> :new: 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.