diff --git a/docs/guide/advanced/composition.md b/docs/guide/advanced/composition.md index ed8272151..0cc50f882 100644 --- a/docs/guide/advanced/composition.md +++ b/docs/guide/advanced/composition.md @@ -375,6 +375,100 @@ const { t, d, n, tm, locale } = useI18n({ // Something to do here ... ``` +### Implicit `useScope` Resolution + +When using `useI18n`, the `useScope` option has implicit resolution behavior that you should be aware of: + +:::info Default Behavior +If you don't explicitly specify `useScope`, Vue I18n will implicitly determine the scope based on whether the component has an i18n block: +- **With i18n block**: Defaults to `local` scope +- **Without i18n block**: Defaults to `global` scope +::: + +```js +// In a component WITH i18n block + +// This implicitly uses local scope +const { t } = useI18n() // same as useI18n({ useScope: 'local' }) + + +// In a component WITHOUT i18n block (e.g., composables, stores) + +// This implicitly uses global scope +const { t } = useI18n() // same as useI18n({ useScope: 'global' }) +``` + +This explicit approach prevents unexpected behavior and makes your code more maintainable. + +### Avoiding Multiple useI18n Calls + +:::warning IMPORTANT +**Do not call `useI18n` with local scope multiple times within the same component.** When you call `useI18n` with local scope more than once in the same component, it will not work properly and Vue I18n will emit a warning. +::: + +#### Bad: Multiple calls to useI18n with local scope + +```js +export default { + setup() { + // First call - creates a new Composer instance + const { t } = useI18n({ + locale: 'en', + messages: { + en: { hello: 'Hello' }, + ja: { hello: 'こんにちは' } + } + }) + + // Second call - creates another Composer instance (triggers warning) + const { locale } = useI18n({ + locale: 'en', + messages: { + en: { world: 'World' }, + ja: { world: '世界' } + } + }) + + // These instances are not synchronized! + return { t, locale } + } +} +``` + +#### Good: Single call to useI18n + +```js +export default { + setup() { + // Destructure all needed properties from a single call + const { t, locale, tm, d, n } = useI18n({ + locale: 'en', + messages: { + en: { + hello: 'Hello', + world: 'World' + }, + ja: { + hello: 'こんにちは', + world: '世界' + } + } + }) + + return { t, locale } + } +} +``` + +When you violate this rule, you'll see the following warning in development mode: + +``` +[Vue I18n warn]: Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component. +``` + +If you need to use i18n features in multiple places within your component, destructure all the needed properties from a single `useI18n` call or store the returned object and access its properties as needed. + + ### Locale messages If you use i18n custom blocks in SFC as i18n resource of locale messages, it will be merged with the locale messages specified by the `messages` option of `useI18n`. diff --git a/packages/vue-i18n-core/src/errors.ts b/packages/vue-i18n-core/src/errors.ts index ce9bc2726..521394875 100644 --- a/packages/vue-i18n-core/src/errors.ts +++ b/packages/vue-i18n-core/src/errors.ts @@ -26,9 +26,7 @@ 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, - // duplicate `useI18n` calling - DUPLICATE_USE_I18N_CALLING: 35 + NOT_AVAILABLE_COMPOSITION_IN_LEGACY: 34 } as const type I18nErrorCodes = (typeof I18nErrorCodes)[keyof typeof I18nErrorCodes] @@ -59,7 +57,5 @@ 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', - [I18nErrorCodes.DUPLICATE_USE_I18N_CALLING]: - "Duplicate `useI18n` calling by local scope. Please don't call it on local scope" + 'Not available Compostion API in Legacy API mode. Please make sure that the legacy API mode is working properly' } diff --git a/packages/vue-i18n-core/src/i18n.ts b/packages/vue-i18n-core/src/i18n.ts index 75cb6e39f..17accb856 100644 --- a/packages/vue-i18n-core/src/i18n.ts +++ b/packages/vue-i18n-core/src/i18n.ts @@ -774,7 +774,7 @@ export function useI18n< i18nInternal.__setInstance(instance, composer) } else { if (__DEV__ && scope === 'local') { - throw createI18nError(I18nErrorCodes.DUPLICATE_USE_I18N_CALLING) + warn(getWarnMessage(I18nWarnCodes.DUPLICATE_USE_I18N_CALLING)) } } diff --git a/packages/vue-i18n-core/src/warnings.ts b/packages/vue-i18n-core/src/warnings.ts index 5cea1f658..95162b04e 100644 --- a/packages/vue-i18n-core/src/warnings.ts +++ b/packages/vue-i18n-core/src/warnings.ts @@ -12,7 +12,9 @@ export const I18nWarnCodes = { /** * @deprecated will be removed at vue-i18n v12 */ - DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12 + DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE: 12, + // duplicate `useI18n` calling + DUPLICATE_USE_I18N_CALLING: 13 } as const type I18nWarnCodes = (typeof I18nWarnCodes)[keyof typeof I18nWarnCodes] @@ -28,7 +30,9 @@ export const warnMessages: { [code: number]: string } = { /** * @deprecated will be removed at vue-i18n v12 */ - [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.` + [I18nWarnCodes.DEPRECATE_TRANSLATE_CUSTOME_DIRECTIVE]: `'v-t' has been deprecated in v11. Use translate APIs ('t' or '$t') instead.`, + [I18nWarnCodes.DUPLICATE_USE_I18N_CALLING]: + "Duplicate `useI18n` calling by local scope. Please don't call it on local scope, due to it does not work properly in component." } export function getWarnMessage( diff --git a/packages/vue-i18n-core/test/i18n.test.ts b/packages/vue-i18n-core/test/i18n.test.ts index 9707cb07f..aacb0349b 100644 --- a/packages/vue-i18n-core/test/i18n.test.ts +++ b/packages/vue-i18n-core/test/i18n.test.ts @@ -35,6 +35,7 @@ import { import { Composer } from '../src/composer' import { errorMessages, I18nErrorCodes } from '../src/errors' import { createI18n, useI18n } from '../src/i18n' +import { I18nWarnCodes, warnMessages } from '../src/warnings' import { pluralRules as _pluralRules, mount, randStr } from './helper' import type { IntlifyDevToolsEmitterHooks } from '@intlify/devtools-types' @@ -623,7 +624,11 @@ describe('useI18n', () => { ) }) - test(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING], async () => { + test(warnMessages[I18nWarnCodes.DUPLICATE_USE_I18N_CALLING], async () => { + const mockWarn = vi.spyOn(shared, 'warn') + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockWarn.mockImplementation(() => {}) + const i18n = createI18n({ legacy: false, locale: 'en', @@ -645,26 +650,22 @@ describe('useI18n', () => { 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!' - } + 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 } + } + }) + t = i18n.t + const ret = useMyComposable() + useMyComposable() + message = ret.message + return { t, message } }, template: `

Root

@@ -676,11 +677,12 @@ describe('useI18n', () => {

{{ t('hi') }}

{{ message }}

-

{{ error }}

` }) await mount(App, i18n as any) // eslint-disable-line @typescript-eslint/no-explicit-any - expect(error).toBe(errorMessages[I18nErrorCodes.DUPLICATE_USE_I18N_CALLING]) + expect(mockWarn.mock.calls[0][0]).toBe( + warnMessages[I18nWarnCodes.DUPLICATE_USE_I18N_CALLING] + ) }) })