Skip to content

Commit

Permalink
fix: crash of the app when we mix flat json keys (#1419)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Jun 5, 2023
1 parent 9961aa8 commit 073c9d0
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 7 deletions.
21 changes: 18 additions & 3 deletions packages/vue-i18n-core/src/utils.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]])
Expand Down
6 changes: 4 additions & 2 deletions packages/vue-i18n-core/src/warnings.ts
Expand Up @@ -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]
Expand All @@ -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(
Expand Down
Expand Up @@ -8,4 +8,6 @@ exports[`issue #1054, #1053 1`] = `"<p>+$123,456.79</p><span>+$123,456.79</span>
exports[`issue #1055 1`] = `"<p>John opened issue 123</p>"`;
exports[`issue #1373 1`] = `"<p class=\\"name\\">hello, <span>kazupon</span>!</p><p>6/2/2023</p><p>$100.00</p>"`;
exports[`issue #1365 1`] = `"<p>Animal</p>"`;
exports[`issue #1373 1`] = `"<p class=\\"name\\">hello, <span>kazupon</span>!</p><p>6/5/2023</p><p>$100.00</p>"`;
25 changes: 24 additions & 1 deletion packages/vue-i18n-core/test/issues.test.ts
Expand Up @@ -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: `
<p>{{ $t('animal') }}</p>
`
})

const wrapper = await mount(App, i18n)

expect(wrapper.html()).toMatchSnapshot()
})

test('issue #1373', async () => {
const i18n = createI18n({
locale: 'en-US',
Expand All @@ -887,7 +910,7 @@ test('issue #1373', async () => {
<span>kazupon</span>
</template>
</I18nT>
<I18nD tag="p" :value="new Date()"></I18nD>
<I18nD tag="p" :value="new Date(1685951676578)"></I18nD>
<I18nN tag="p" :value="100" format="currency"></I18nN>
`
})
Expand Down
38 changes: 38 additions & 0 deletions packages/vue-i18n-core/test/utils.test.ts
@@ -1,13 +1,35 @@
// utils
import * as shared from '@intlify/shared'
vi.mock('@intlify/shared', async () => {
const actual = await vi.importActual<object>('@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',
'b.x': {
'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 = {
Expand All @@ -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'
})
)
})

0 comments on commit 073c9d0

Please sign in to comment.