Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: crash of the app when we mix flat json keys #1419

Merged
merged 1 commit into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions packages/vue-i18n-core/src/utils.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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'
})
)
})