diff --git a/packages/core-base/src/context.ts b/packages/core-base/src/context.ts index bf0a64553..3104e1c79 100644 --- a/packages/core-base/src/context.ts +++ b/packages/core-base/src/context.ts @@ -692,4 +692,32 @@ export function updateFallbackLocale( ctx.localeFallbacker(ctx, fallback, locale) } +/** @internal */ +export function isAlmostSameLocale( + locale: Locale, + compareLocale: Locale +): boolean { + if (locale === compareLocale) return false + return locale.split('-')[0] === compareLocale.split('-')[0] +} + +/** @internal */ +export function isImplicitFallback( + targetLocale: Locale, + locales: Locale[] +): boolean { + const index = locales.indexOf(targetLocale) + if (index === -1) { + return false + } + + for (let i = index + 1; i < locales.length; i++) { + if (isAlmostSameLocale(targetLocale, locales[i])) { + return true + } + } + + return false +} + /* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/core-base/src/translate.ts b/packages/core-base/src/translate.ts index 465266a5c..70a54e44e 100644 --- a/packages/core-base/src/translate.ts +++ b/packages/core-base/src/translate.ts @@ -20,6 +20,8 @@ import { isMessageAST } from './compilation' import { createMessageContext } from './runtime' import { isTranslateFallbackWarn, + isAlmostSameLocale, + isImplicitFallback, handleMissing, NOT_REOSLVED, getAdditionalMeta, @@ -839,6 +841,7 @@ function resolveMessageFormat( if ( __DEV__ && locale !== targetLocale && + !isAlmostSameLocale(locale, targetLocale) && isTranslateFallbackWarn(fallbackWarn, key) ) { onWarn( @@ -905,15 +908,17 @@ function resolveMessageFormat( break } - const missingRet = handleMissing( - context as any, // eslint-disable-line @typescript-eslint/no-explicit-any - key, - targetLocale, - missingWarn, - type - ) - if (missingRet !== key) { - format = missingRet as PathValue + if (!isImplicitFallback(targetLocale, locales)) { + const missingRet = handleMissing( + context as any, // eslint-disable-line @typescript-eslint/no-explicit-any + key, + targetLocale, + missingWarn, + type + ) + if (missingRet !== key) { + format = missingRet as PathValue + } } from = to } diff --git a/packages/vue-i18n-core/test/issues.test.ts b/packages/vue-i18n-core/test/issues.test.ts index b9edbf81c..c635eef96 100644 --- a/packages/vue-i18n-core/test/issues.test.ts +++ b/packages/vue-i18n-core/test/issues.test.ts @@ -1359,6 +1359,67 @@ test('issue #1738', async () => { expect(wrapper.find('#te2')?.textContent).toEqual(`true - expected true`) }) +describe('issue #1768', () => { + test('Implicit fallback using locales', async () => { + const mockWarn = vi.spyOn(shared, 'warn') + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockWarn.mockImplementation(() => {}) + + const i18n = createI18n({ + locale: 'en-US', + fallbackLocale: 'en', + messages: { + en: { + hello: { + 'vue-i18n': 'Hello, Vue I18n' + } + } + } + }) + + const App = defineComponent({ + template: `
{{ $t('hello.vue-i18n') }}
` + }) + const wrapper = await mount(App, i18n) + + expect(wrapper.html()).toEqual('
Hello, Vue I18n
') + expect(mockWarn).toHaveBeenCalledTimes(0) + }) + + test('Explicit fallback with decision maps', async () => { + const mockWarn = vi.spyOn(shared, 'warn') + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockWarn.mockImplementation(() => {}) + + const i18n = createI18n({ + locale: 'zh-Hant', + fallbackLocale: { + 'de-CH': ['fr', 'it'], + 'zh-Hant': ['zh-Hans'], + 'es-CL': ['es-AR'], + es: ['en-GB'], + pt: ['es-AR'], + default: ['en', 'da'] + }, + messages: { + zh: { + hello: { + 'vue-i18n': '你好,Vue I18n' + } + } + } + }) + + const App = defineComponent({ + template: `
{{ $t('hello.vue-i18n') }}
` + }) + const wrapper = await mount(App, i18n) + + expect(wrapper.html()).toEqual('
你好,Vue I18n
') + expect(mockWarn).toHaveBeenCalledTimes(0) + }) +}) + test('#1796', async () => { const i18n = createI18n({ locale: 'en',