diff --git a/packages/vue-i18n-core/src/utils.ts b/packages/vue-i18n-core/src/utils.ts index 5a6aa6184..6af257bf6 100644 --- a/packages/vue-i18n-core/src/utils.ts +++ b/packages/vue-i18n-core/src/utils.ts @@ -4,10 +4,12 @@ import { isObject, hasOwn, isPlainObject, - isString + isString, + warn } from '@intlify/shared' import { Text, createVNode } from 'vue' import { I18nErrorCodes, createI18nError } from './errors' +import { I18nWarnCodes, getWarnMessage } from './warnings' import type { Locale, MessageResolver } from '@intlify/core-base' import type { @@ -72,15 +74,28 @@ export function handleFlatJson(obj: unknown): unknown { const subKeys = key.split('.') const lastIndex = subKeys.length - 1 let currentObj = obj + let hasStringValue = false for (let i = 0; i < lastIndex; i++) { if (!(subKeys[i] in currentObj)) { currentObj[subKeys[i]] = {} } + if (!isObject(currentObj[subKeys[i]])) { + __DEV__ && + warn( + getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, { + key: subKeys[i] + }) + ) + hasStringValue = true + break + } currentObj = currentObj[subKeys[i]] } // update last object value, delete old property - currentObj[subKeys[lastIndex]] = obj[key] - delete obj[key] + if (!hasStringValue) { + currentObj[subKeys[lastIndex]] = obj[key] + delete obj[key] + } // recursive process value if value is also a object if (isObject(currentObj[subKeys[lastIndex]])) { handleFlatJson(currentObj[subKeys[lastIndex]]) diff --git a/packages/vue-i18n-core/src/warnings.ts b/packages/vue-i18n-core/src/warnings.ts index d5bde3b86..b5bb046f9 100644 --- a/packages/vue-i18n-core/src/warnings.ts +++ b/packages/vue-i18n-core/src/warnings.ts @@ -11,7 +11,8 @@ export const I18nWarnCodes = { NOT_SUPPORTED_PRESERVE_DIRECTIVE: inc(), // 10 NOT_SUPPORTED_GET_CHOICE_INDEX: inc(), // 11 COMPONENT_NAME_LEGACY_COMPATIBLE: inc(), // 12 - NOT_FOUND_PARENT_SCOPE: inc() // 13 + NOT_FOUND_PARENT_SCOPE: inc(), // 13 + IGNORE_OBJ_FLATTEN: inc() // 14 } as const type I18nWarnCodes = (typeof I18nWarnCodes)[keyof typeof I18nWarnCodes] @@ -23,7 +24,8 @@ export const warnMessages: { [code: number]: string } = { [I18nWarnCodes.NOT_SUPPORTED_PRESERVE_DIRECTIVE]: `Not supported 'preserveDirectiveContent'.`, [I18nWarnCodes.NOT_SUPPORTED_GET_CHOICE_INDEX]: `Not supported 'getChoiceIndex'.`, [I18nWarnCodes.COMPONENT_NAME_LEGACY_COMPATIBLE]: `Component name legacy compatible: '{name}' -> 'i18n'`, - [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.` + [I18nWarnCodes.NOT_FOUND_PARENT_SCOPE]: `Not found parent scope. use the global scope.`, + [I18nWarnCodes.IGNORE_OBJ_FLATTEN]: `Ignore object flatten: '{key}' key has an string value` } export function getWarnMessage( diff --git a/packages/vue-i18n-core/test/__snapshots__/issues.test.ts.snap b/packages/vue-i18n-core/test/__snapshots__/issues.test.ts.snap index ccfb64a21..a994c70c7 100644 --- a/packages/vue-i18n-core/test/__snapshots__/issues.test.ts.snap +++ b/packages/vue-i18n-core/test/__snapshots__/issues.test.ts.snap @@ -8,4 +8,6 @@ exports[`issue #1054, #1053 1`] = `"

+$123,456.79

+$123,456.79 exports[`issue #1055 1`] = `"

John opened issue 123

"`; -exports[`issue #1373 1`] = `"

hello, kazupon!

6/2/2023

$100.00

"`; +exports[`issue #1365 1`] = `"

Animal

"`; + +exports[`issue #1373 1`] = `"

hello, kazupon!

6/5/2023

$100.00

"`; diff --git a/packages/vue-i18n-core/test/issues.test.ts b/packages/vue-i18n-core/test/issues.test.ts index 23d19e760..39f7159a0 100644 --- a/packages/vue-i18n-core/test/issues.test.ts +++ b/packages/vue-i18n-core/test/issues.test.ts @@ -872,6 +872,29 @@ test('issue #1123', async () => { ) }) +test('issue #1365', async () => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + flatJson: true, + messages: { + en: { + 'animal.dog': 'Dog', + animal: 'Animal' + } + } + }) + const App = defineComponent({ + template: ` +

{{ $t('animal') }}

+ ` + }) + + const wrapper = await mount(App, i18n) + + expect(wrapper.html()).toMatchSnapshot() +}) + test('issue #1373', async () => { const i18n = createI18n({ locale: 'en-US', @@ -887,7 +910,7 @@ test('issue #1373', async () => { kazupon - + ` }) diff --git a/packages/vue-i18n-core/test/utils.test.ts b/packages/vue-i18n-core/test/utils.test.ts index 7b7f0a81a..751b75fb4 100644 --- a/packages/vue-i18n-core/test/utils.test.ts +++ b/packages/vue-i18n-core/test/utils.test.ts @@ -1,6 +1,20 @@ +// utils +import * as shared from '@intlify/shared' +vi.mock('@intlify/shared', async () => { + const actual = await vi.importActual('@intlify/shared') + return { + ...actual, + warn: vi.fn() + } +}) import { handleFlatJson } from '../src/utils' +import { I18nWarnCodes, getWarnMessage } from '../src/warnings' test('handleFlatJson', () => { + const mockWarn = vi.spyOn(shared, 'warn') + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockWarn.mockImplementation(() => {}) + const obj = { a: { a1: 'a1.value' }, 'a.a2': 'a.a2.value', @@ -8,6 +22,14 @@ test('handleFlatJson', () => { 'b1.x': 'b1.x.value', 'b2.x': ['b2.x.value0', 'b2.x.value1'], 'b3.x': { 'b3.x': 'b3.x.value' } + }, + c: { + 'animal.dog': 'Dog', + animal: 'Animal' + }, + d: { + 'animal.dog': 'Dog', + animal: {} } } const expectObj = { @@ -21,7 +43,23 @@ test('handleFlatJson', () => { b2: { x: ['b2.x.value0', 'b2.x.value1'] }, b3: { x: { b3: { x: 'b3.x.value' } } } } + }, + c: { + 'animal.dog': 'Dog', + animal: 'Animal' + }, + d: { + animal: { + dog: 'Dog' + } } } + expect(handleFlatJson(obj)).toEqual(expectObj) + expect(mockWarn).toHaveBeenCalled() + expect(mockWarn.mock.calls[0][0]).toEqual( + getWarnMessage(I18nWarnCodes.IGNORE_OBJ_FLATTEN, { + key: 'animal' + }) + ) })