Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
"packageManager": "pnpm@9.0.4",
"pnpm": {
"overrides": {
"vue": "3.5.13",
"vue": "3.6.0-alpha.4",
"vite": "^6.0.0"
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/vue-i18n-core/src/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
isString,
warn
} from '@intlify/shared'
import { computed, getCurrentInstance, ref, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { I18nErrorCodes, createI18nError } from './errors'
import { VERSION } from './misc'
import {
Expand All @@ -53,6 +53,7 @@ import {
import {
createTextNode,
getComponentOptions,
getCurrentInstance,
getLocaleMessages,
handleFlatJson
} from './utils'
Expand Down Expand Up @@ -109,6 +110,7 @@ import type { VueDevToolsEmitter } from '@intlify/devtools-types'
import type {
ComponentInternalInstance,
ComputedRef,
GenericComponentInstance,
VNode,
VNodeArrayChildren,
WritableComputedRef
Expand Down Expand Up @@ -226,7 +228,7 @@ export type DefaultNumberFormatSchema<
export type MissingHandler = (
locale: Locale,
key: Path,
instance?: ComponentInternalInstance,
instance?: ComponentInternalInstance | GenericComponentInstance,
type?: string
) => string | void

Expand Down
32 changes: 18 additions & 14 deletions packages/vue-i18n-core/src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import type {
HookPayloads,
InspectedComponentData
} from '@vue/devtools-api'
import type { App, ComponentInternalInstance } from 'vue'
import type {
App,
ComponentInternalInstance,
GenericComponentInstance
} from 'vue'
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Devtools now bind to instance.__VUE_I18N__, consistent with new lifecycle wiring

Reading the composer from componentInstance.__VUE_I18N__ (and tagging tree nodes only when it differs from the global composer) matches how setupLifeCycle in i18n.ts now attaches/removes the composer on component instances, including generic ones. The widened roots and getComponentInstance typings to ComponentInternalInstance | GenericComponentInstance are in line with that model.

Also applies to: 72-97, 115-118, 168-189, 321-327


🏁 Script executed:

#!/bin/bash
# Inspect all uses of `__VUE_I18N__` to ensure devtools and lifecycle wiring are consistent.
rg -n "__VUE_I18N__" packages/vue-i18n-core -C3

Length of output: 7001


Critical inconsistency: devtools reads from component instance, but mixin still attaches to DOM element

The verification reveals a mismatch between binding locations:

  • i18n.ts (setupLifeCycle, composition API): attaches __VUE_I18N__ to the component instance (target.__VUE_I18N__)
  • devtools.ts: reads from the component instance (componentInstance.__VUE_I18N__)
  • mixins.ts (options API): attaches __VUE_I18N__ to the DOM element (this.$el.__VUE_I18N__)

The devtools code will not discover composers attached via the mixin path, breaking devtools inspection for legacy/options-API components. Either mixins.ts needs to attach to the component instance instead of the DOM element, or devtools needs to fall back to checking the DOM element for legacy compatibility.

🤖 Prompt for AI Agents
In packages/vue-i18n-core/src/devtools.ts around lines 27 to 31, devtools
currently only reads __VUE_I18N__ from the component instance but mixins attach
it to the DOM element; update devtools to fallback to checking the component's
root DOM element if componentInstance.__VUE_I18N__ is undefined (i.e., check
componentInstance.vnode?.el or componentInstance?.proxy?.$el for __VUE_I18N__),
so it discovers composers attached via the options/mixin path; implement the
fallback lookup and use whichever composer object is found.

import type { Composer } from './composer'
import type { I18n, I18nInternal } from './i18n'
import type { VueI18nInternal } from './legacy'
Expand Down Expand Up @@ -70,26 +74,22 @@ export async function enableDevTools(app: App, i18n: _I18n): Promise<boolean> {
})

api.on.inspectComponent(({ componentInstance, instanceData }) => {
if (
componentInstance.vnode.el &&
componentInstance.vnode.el.__VUE_I18N__ &&
instanceData
) {
if (componentInstance.__VUE_I18N__ && instanceData) {
if (i18n.mode === 'legacy') {
// ignore global scope on legacy mode
if (
componentInstance.vnode.el.__VUE_I18N__ !==
componentInstance.__VUE_I18N__ !==
(i18n.global as unknown as VueI18nInternal).__composer
) {
inspectComposer(
instanceData,
componentInstance.vnode.el.__VUE_I18N__ as Composer
componentInstance.__VUE_I18N__ as Composer
)
}
} else {
inspectComposer(
instanceData,
componentInstance.vnode.el.__VUE_I18N__ as Composer
componentInstance.__VUE_I18N__ as Composer
)
}
}
Expand All @@ -112,7 +112,10 @@ export async function enableDevTools(app: App, i18n: _I18n): Promise<boolean> {
}
})

const roots = new Map<App, ComponentInternalInstance>()
const roots = new Map<
App,
ComponentInternalInstance | GenericComponentInstance
>()
api.on.getInspectorState(async payload => {
if (
payload.app === app &&
Expand Down Expand Up @@ -180,9 +183,9 @@ function updateComponentTreeTags(
const global = i18n.mode === 'composition'
? i18n.global
: (i18n.global as unknown as VueI18nInternal).__composer
if (instance && instance.vnode.el && instance.vnode.el.__VUE_I18N__) {
if (instance && instance.__VUE_I18N__) {
// add custom tags local scope only
if (instance.vnode.el.__VUE_I18N__ !== global) {
if (instance.__VUE_I18N__ !== global) {
const tag = {
label: `i18n (${getI18nScopeLable(instance)} Scope)`,
textColor: 0x000000,
Expand Down Expand Up @@ -318,8 +321,9 @@ function registerScope(
function getComponentInstance(
nodeId: string,
i18n: _I18n
): ComponentInternalInstance | null {
let instance: ComponentInternalInstance | null = null
): ComponentInternalInstance | GenericComponentInstance | null {
let instance: ComponentInternalInstance | GenericComponentInstance | null =
null

if (nodeId !== 'global') {
for (const [component, composer] of i18n.__instances.entries()) {
Expand Down
90 changes: 57 additions & 33 deletions packages/vue-i18n-core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import {
InjectionKey,
effectScope,
getCurrentInstance,
inject,
isRef,
onMounted,
Expand All @@ -29,7 +28,11 @@ import {
EnableEmitter,
InejctWithOptionSymbol
} from './symbols'
import { adjustI18nResources, getComponentOptions } from './utils'
import {
adjustI18nResources,
getComponentOptions,
getCurrentInstance
} from './utils'
import { I18nWarnCodes, getWarnMessage } from './warnings'

import type {
Expand All @@ -42,7 +45,12 @@ import type {
VueDevToolsEmitter,
VueDevToolsEmitterEvents
} from '@intlify/devtools-types'
import type { App, ComponentInternalInstance, EffectScope } from 'vue'
import type {
App,
ComponentInternalInstance,
EffectScope,
GenericComponentInstance
} from 'vue'
import type {
Composer,
ComposerInternalOptions,
Expand Down Expand Up @@ -73,9 +81,27 @@ declare module 'vue' {
export interface ComponentInternalInstance {
/**
* @internal
* is custom element?
* whether target component is custom element
*/
isCE?: boolean
/**
* @internal
* for vue/devtools i18n composer hook
*/
__VUE_I18N__?: Composer
}

export interface GenericComponentInstance {
/**
* @internal
* whether target component is custom element
*/
isCE?: boolean
/**
* @internal
* for vue/devtools i18n composer hook
*/
__VUE_I18N__?: Composer
}
}

Expand Down Expand Up @@ -240,7 +266,7 @@ export interface I18nInternal<
OptionLocale = Locale
> {
__instances: Map<
ComponentInternalInstance,
ComponentInternalInstance | GenericComponentInstance,
| VueI18n<Messages, DateTimeFormats, NumberFormats, OptionLocale>
| Composer<Messages, DateTimeFormats, NumberFormats, OptionLocale>
>
Expand All @@ -249,17 +275,19 @@ export interface I18nInternal<
| VueI18n<Messages, DateTimeFormats, NumberFormats, OptionLocale>
| Composer<Messages, DateTimeFormats, NumberFormats, OptionLocale>
>(
component: ComponentInternalInstance
component: ComponentInternalInstance | GenericComponentInstance
): Instance | null
__setInstance<
Instance extends
| VueI18n<Messages, DateTimeFormats, NumberFormats, OptionLocale>
| Composer<Messages, DateTimeFormats, NumberFormats, OptionLocale>
>(
component: ComponentInternalInstance,
component: ComponentInternalInstance | GenericComponentInstance,
instance: Instance
): void
__deleteInstance(component: ComponentInternalInstance): void
__deleteInstance(
component: ComponentInternalInstance | GenericComponentInstance
): void
__composerExtend?: ComposerExtender
/**
* @deprecated will be removed at vue-i18n v12
Expand Down Expand Up @@ -495,24 +523,29 @@ export function createI18n(options: any = {}): any {
const __globalInjection = isBoolean(options.globalInjection)
? options.globalInjection
: true
const __instances = new Map<ComponentInternalInstance, VueI18n | Composer>()
const __instances = new Map<
ComponentInternalInstance | GenericComponentInstance,
VueI18n | Composer
>()
const [globalScope, __global] = createGlobal(options, __legacyMode)
const symbol: InjectionKey<I18n> | string = /* #__PURE__*/ makeSymbol(
__DEV__ ? 'vue-i18n' : ''
)

function __getInstance<Instance extends VueI18n | Composer>(
component: ComponentInternalInstance
component: ComponentInternalInstance | GenericComponentInstance
): Instance | null {
return (__instances.get(component) as unknown as Instance) || null
}
function __setInstance<Instance extends VueI18n | Composer>(
component: ComponentInternalInstance,
component: ComponentInternalInstance | GenericComponentInstance,
instance: Instance
): void {
__instances.set(component, instance)
}
function __deleteInstance(component: ComponentInternalInstance): void {
function __deleteInstance(
component: ComponentInternalInstance | GenericComponentInstance
): void {
__instances.delete(component)
}

Expand Down Expand Up @@ -801,7 +834,9 @@ function createGlobal(
return [scope, obj]
}

function getI18nInstance(instance: ComponentInternalInstance): I18n {
function getI18nInstance(
instance: ComponentInternalInstance | GenericComponentInstance
): I18n {
const i18n = inject(
!instance.isCE
? instance.appContext.app.__VUE_I18N_SYMBOL__!
Expand Down Expand Up @@ -839,15 +874,13 @@ function getGlobalComposer(i18n: I18n): Composer {

function getComposer(
i18n: I18n,
target: ComponentInternalInstance,
target: ComponentInternalInstance | GenericComponentInstance,
useComponent = false
): Composer | null {
let composer: Composer | null = null
const root = target.root
let current: ComponentInternalInstance | null = getParentComponentInstance(
target,
useComponent
)
let current: ComponentInternalInstance | GenericComponentInstance | null =
getParentComponentInstance(target, useComponent)
while (current != null) {
const i18nInternal = i18n as unknown as I18nInternal
if (i18n.mode === 'composition') {
Expand Down Expand Up @@ -880,7 +913,7 @@ function getComposer(
}

function getParentComponentInstance(
target: ComponentInternalInstance | null,
target: ComponentInternalInstance | GenericComponentInstance | null,
useComponent = false
) {
if (target == null) {
Expand All @@ -894,19 +927,15 @@ function getParentComponentInstance(

function setupLifeCycle(
i18n: I18nInternal,
target: ComponentInternalInstance,
target: ComponentInternalInstance | GenericComponentInstance,
composer: Composer
): void {
let emitter: VueDevToolsEmitter | null = null

onMounted(() => {
// inject composer instance to DOM for intlify-devtools
if (
(__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) &&
!__NODE_JS__ &&
target.vnode.el
) {
target.vnode.el.__VUE_I18N__ = composer
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
target.__VUE_I18N__ = composer
emitter = createEmitter<VueDevToolsEmitterEvents>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _composer = composer as any
Expand All @@ -920,15 +949,10 @@ function setupLifeCycle(
const _composer = composer as any

// remove composer instance from DOM for intlify-devtools
if (
(__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) &&
!__NODE_JS__ &&
target.vnode.el &&
target.vnode.el.__VUE_I18N__
) {
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
emitter && emitter.off('*', addTimelineEvent)
_composer[DisableEmitter] && _composer[DisableEmitter]()
delete target.vnode.el.__VUE_I18N__
delete target.__VUE_I18N__
}
i18n.__deleteInstance(target)

Expand Down
14 changes: 13 additions & 1 deletion packages/vue-i18n-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
isString,
warn
} from '@intlify/shared'
import * as Vue from 'vue'
import { Text, createVNode } from 'vue'
import { I18nWarnCodes, getWarnMessage } from './warnings'
Comment on lines +13 to 15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Centralized getCurrentInstance/GenericComponentInstance support looks consistent

Importing * as Vue and introducing getCurrentInstance() plus a ComponentInternalInstance | GenericComponentInstance-aware getComponentOptions() lines up with the wider instance usage in composer, i18n, and devtools. The Vue.currentInstance || Vue.getCurrentInstance() fallback is a reasonable way to bridge Vue 3.6 behavior with earlier runtimes in one place.

Also applies to: 19-23, 168-172, 222-228


🏁 Script executed:

#!/bin/bash
# Check for any remaining direct `getCurrentInstance` imports/usages now that utils exposes a wrapper.
rg -n "getCurrentInstance" packages/vue-i18n-core -C3

Length of output: 9739


Update mixins.ts to use the getCurrentInstance wrapper from utils

The centralization goal is undermined: packages/vue-i18n-core/src/mixins.ts imports getCurrentInstance directly from 'vue' (line 2) instead of from './utils'. This bypasses the Vue 3.6 compatibility wrapper entirely. Lines 44 and 134 in mixins.ts should use the wrapper to ensure consistent handling.

Change line 2 in mixins.ts:

- import { getCurrentInstance } from 'vue'
+ import { getCurrentInstance } from './utils'
🤖 Prompt for AI Agents
In packages/vue-i18n-core/src/mixins.ts around lines 44 and 134, the file
imports getCurrentInstance directly from 'vue' instead of using the
compatibility wrapper in ./utils; update the import statement to import
getCurrentInstance from './utils' (remove it from the named imports from 'vue'
and add it to the local utils import) so both usages at the specified lines call
the wrapper, preserving Vue 3.6 compatibility and centralized behavior.


import type { Locale, MessageResolver } from '@intlify/core-base'
import type {
ComponentInternalInstance,
GenericComponentInstance,
RendererElement,
RendererNode
} from 'vue'
Expand Down Expand Up @@ -163,7 +165,9 @@ export function getLocaleMessages<Messages = {}>(
return ret as { [K in keyof Messages]: Messages[K] }
}

export function getComponentOptions(instance: ComponentInternalInstance): any {
export function getComponentOptions(
instance: ComponentInternalInstance | GenericComponentInstance
): any {
return instance.type
}

Expand Down Expand Up @@ -214,3 +218,11 @@ export function adjustI18nResources(
export function createTextNode(key: string): any {
return createVNode(Text, null, key, 0)
}

export function getCurrentInstance():
| GenericComponentInstance
| ComponentInternalInstance
| null {
// @ts-ignore -- NOTE(kazupon): for Vue 3.6
return Vue.currentInstance || Vue.getCurrentInstance()
}
2 changes: 1 addition & 1 deletion packages/vue-i18n-core/test/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import {
ComponentOptions,
defineComponent,
defineCustomElement,
getCurrentInstance,
h,
nextTick,
ref
} from 'vue'
import { Composer } from '../src/composer'
import { errorMessages, I18nErrorCodes } from '../src/errors'
import { createI18n, useI18n } from '../src/i18n'
import { getCurrentInstance } from '../src/utils'
import { I18nWarnCodes, warnMessages } from '../src/warnings'
import { pluralRules as _pluralRules, mount, randStr } from './helper'

Expand Down
Loading
Loading