Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 8 additions & 4 deletions packages/vue-i18n-core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
createCompileError,
CORE_ERROR_CODES_EXTEND_POINT
CORE_ERROR_CODES_EXTEND_POINT,
createCompileError
} from '@intlify/core-base'

import type { BaseError } from '@intlify/shared'
Expand All @@ -26,7 +26,9 @@ export const I18nErrorCodes = {
// not compatible legacy vue-i18n constructor
NOT_COMPATIBLE_LEGACY_VUE_I18N: 33,
// Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly
NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34
NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34,
// duplicate `useI18n` calling
DUPLICATE_USE_I18N_CALLING: 35
} as const

type I18nErrorCodes = (typeof I18nErrorCodes)[keyof typeof I18nErrorCodes]
Expand Down Expand Up @@ -57,5 +59,7 @@ export const errorMessages: { [code: number]: string } = {
[I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]:
'Not compatible legacy VueI18n.',
[I18nErrorCodes.NOT_AVAILABLE_COMPOSITION_IN_LEGACY]:
'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly'
'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly',
[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING]:
"Duplicate `useI18n` calling by local scope. Please don't call it on local scope"
}
4 changes: 4 additions & 0 deletions packages/vue-i18n-core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,10 @@ export function useI18n<
setupLifeCycle(i18nInternal, instance, composer)

i18nInternal.__setInstance(instance, composer)
} else {
if (scope === 'local') {
throw createI18nError(I18nErrorCodes.DUPLICATE_USE_I18N_CALLING)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

❓ Verification inconclusive

Throwing on second local call is breaking; document the change

The new block throws on a second useI18n with scope === 'local'.
This alters previous behaviour (which returned the existing composer) and will break code that legitimately re-invoked useI18n to get the same instance (a common pattern in composables).

  1. Confirm this behavioural change is intended and worth the break.
  2. If so, add a migration note / dev-only warning to guide users upgrading from v10.

Optionally, gate the throw behind __DEV__ to avoid production crashes while still warning during development.


Throwing on second local call is breaking; document the change

The new block throws on a second useI18n with scope === 'local'.
This alters previous behaviour (which returned the existing composer) and will break code that legitimately re-invoked useI18n to get the same instance (a common pattern in composables).

  1. Confirm this behavioural change is intended and worth the break.
  2. If so, add a migration note / dev-only warning to guide users upgrading from v10.

Optionally, gate the throw behind __DEV__ to avoid production crashes while still warning during development.

🤖 Prompt for AI Agents
In packages/vue-i18n-core/src/i18n.ts around lines 775 to 778, the new code
throws an error on a second call to useI18n with scope 'local', changing
previous behavior that returned the existing composer instance. Confirm if this
breaking change is intended; if yes, add a migration note or a development-only
warning to inform users upgrading from v10. Additionally, modify the throw to
only occur in development mode (__DEV__) to prevent production crashes while
still alerting developers.

}

return composer as unknown as Composer<
Expand Down
2 changes: 2 additions & 0 deletions packages/vue-i18n-core/test/__snapshots__/i18n.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ exports[`slot reactivity > composable > ja 1`] = `"<h1>Root</h1><form><select><o
exports[`slot reactivity > legacy > en 1`] = `"<h1>Root</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>hello!</p><div class="child"><h1>Child</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>hello!</p><div class="sub-child"><h1>Sub Child</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>hello!</p></div> $t inside of slot <p>hello!</p> i18n-t inside of slot <p>hello!</p></div>"`;

exports[`slot reactivity > legacy > ja 1`] = `"<h1>Root</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>こんにちは!</p><div class="child"><h1>Child</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>こんにちは!</p><div class="sub-child"><h1>Sub Child</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>こんにちは!</p></div> $t inside of slot <p>こんにちは!</p> i18n-t inside of slot <p>こんにちは!</p></div>"`;

exports[`useI18n > Duplicate \`useI18n\` calling by local scope. Please don't call it on local scope 1`] = `"<h1>Root</h1><form><select><option value="en">en</option><option value="ja">ja</option></select></form><p>hi!</p><p></p><p>Duplicate \`useI18n\` calling by local scope. Please don't call it on local scope</p>"`;
60 changes: 60 additions & 0 deletions packages/vue-i18n-core/test/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,66 @@ describe('useI18n', () => {
errorMessages[I18nErrorCodes.NOT_INSTALLED_WITH_PROVIDE]
)
})

test(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING], async () => {
const i18n = createI18n<false>({
legacy: false,
locale: 'en',
fallbackLocale: ['en'],
messages: {
en: { hello: 'hello!' }
}
})

const useMyComposable = () => {
const count = ref(0)
const { t } = useI18n({
messages: {
en: {
there: 'hi there! {count}'
}
}
})
return { message: t('there', { count: count.value }) }
}

let error = ''
const App = defineComponent({
setup() {
let message: string = ''
let t: any // eslint-disable-line @typescript-eslint/no-explicit-any
try {
const i18n = useI18n({
messages: {
en: {
hi: 'hi!'
}
}
})
t = i18n.t
const ret = useMyComposable()
message = ret.message
} catch (e: any) {
error = e.message
}
return { t, message, error }
},
template: `
<h1>Root</h1>
<form>
<select v-model="locale">
<option value="en">en</option>
<option value="ja">ja</option>
</select>
</form>
<p>{{ t('hi') }}</p>
<p>{{ message }}</p>
<p>{{ error }}</p>
`
})
const { html } = await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(html()).toMatchSnapshot()
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

❓ Verification inconclusive

Prefer explicit error assertion over generic snapshot

The goal of this test is to verify that a duplicate-scope useI18n() call surfaces DUPLICATE_USE_I18N_CALLING.
Relying on a full-HTML snapshot makes the test brittle (any innocuous template change or formatting tweak will break the snapshot while the functional contract is still satisfied).

A more focused assertion keeps the intent clear and reduces maintenance noise:

-const { html } = await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
-expect(html()).toMatchSnapshot()
+await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
+expect(error).toBe(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING])

This directly validates the thrown error and avoids snapshot churn.


Prefer explicit error assertion over generic snapshot

The goal of this test is to verify that a duplicate-scope useI18n() call surfaces DUPLICATE_USE_I18N_CALLING.
Relying on a full-HTML snapshot makes the test brittle (any innocuous template change or formatting tweak will break the snapshot while the functional contract is still satisfied).

A more focused assertion keeps the intent clear and reduces maintenance noise:

-const { html } = await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
-expect(html()).toMatchSnapshot()
+await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
+expect(error).toBe(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING])

This directly validates the thrown error and avoids snapshot churn.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING], async () => {
const i18n = createI18n<false>({
legacy: false,
locale: 'en',
fallbackLocale: ['en'],
messages: {
en: { hello: 'hello!' }
}
})
const useMyComposable = () => {
const count = ref(0)
const { t } = useI18n({
messages: {
en: {
there: 'hi there! {count}'
}
}
})
return { message: t('there', { count: count.value }) }
}
let error = ''
const App = defineComponent({
setup() {
let message: string = ''
let t: any // eslint-disable-line @typescript-eslint/no-explicit-any
try {
const i18n = useI18n({
messages: {
en: {
hi: 'hi!'
}
}
})
t = i18n.t
const ret = useMyComposable()
message = ret.message
} catch (e: any) {
error = e.message
}
return { t, message, error }
},
template: `
<h1>Root</h1>
<form>
<select v-model="locale">
<option value="en">en</option>
<option value="ja">ja</option>
</select>
</form>
<p>{{ t('hi') }}</p>
<p>{{ message }}</p>
<p>{{ error }}</p>
`
})
const { html } = await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(html()).toMatchSnapshot()
})
const App = defineComponent({
setup() {
let message: string = ''
let t: any // eslint-disable-line @typescript-eslint/no-explicit-any
try {
const i18n = useI18n({
messages: {
en: {
hi: 'hi!'
}
}
})
t = i18n.t
const ret = useMyComposable()
message = ret.message
} catch (e: any) {
error = e.message
}
return { t, message, error }
},
template: `
<h1>Root</h1>
<form>
<select v-model="locale">
<option value="en">en</option>
<option value="ja">ja</option>
</select>
</form>
<p>{{ t('hi') }}</p>
<p>{{ message }}</p>
<p>{{ error }}</p>
`
})
- const { html } = await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
- expect(html()).toMatchSnapshot()
+ await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any
+ expect(error).toBe(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING])
})
🤖 Prompt for AI Agents
In packages/vue-i18n-core/test/i18n.test.ts between lines 626 and 684, the test
currently uses a full HTML snapshot to verify the duplicate useI18n call error,
which is brittle and can break with unrelated template changes. Instead,
refactor the test to explicitly assert that the error message matches the
expected DUPLICATE_USE_I18N_CALLING error string. Remove the snapshot assertion
and replace it with a direct check on the error variable to confirm the correct
error is thrown, making the test more focused and maintainable.

})

describe('slot reactivity', () => {
Expand Down
1 change: 1 addition & 0 deletions v11/component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Components
Loading