Conversation
📝 WalkthroughWalkthroughIntroduces an 'isolated' scope option to useI18n, enabling composables to create independent Composer instances separate from component local scopes, preventing message key conflicts. Corresponding documentation is added in three languages, core types and logic are updated, and comprehensive tests are included. Changes
Sequence DiagramsequenceDiagram
participant Component
participant Composable
participant IsolatedComposer as Isolated Composer
participant GlobalComposer as Global Composer
Component->>Component: mount (has local scope)
Composable->>IsolatedComposer: useI18n({ useScope: 'isolated', messages: {...} })
IsolatedComposer->>IsolatedComposer: create independent instance<br/>(not linked to component UID)
IsolatedComposer->>GlobalComposer: inherit locale
Component->>IsolatedComposer: t('key.in.composable')
IsolatedComposer-->>Component: return translation
alt key missing in isolated
IsolatedComposer->>GlobalComposer: fallback to root/global
GlobalComposer-->>IsolatedComposer: return global translation
end
Component->>Component: unmount
IsolatedComposer->>IsolatedComposer: cleanup via onScopeDispose
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying vue-i18n-next with
|
| Latest commit: |
0c291d3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8d0c6f08.vue-i18n-next.pages.dev |
| Branch Preview URL: | https://feat-2207.vue-i18n-next.pages.dev |
Size ReportBundles
Usages
|
@intlify/core
@intlify/core-base
@intlify/devtools-types
@intlify/message-compiler
petite-vue-i18n
@intlify/shared
vue-i18n
@intlify/vue-i18n-core
commit: |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/vue-i18n-core/src/i18n.ts (1)
566-610: Consider extracting shared composer lifecycle wiring forisolatedandlocalpaths.Lines 570–607 duplicate composer extension, devtools emitter setup, and disposal logic also present in the local branch, which increases drift risk.
♻️ Suggested refactor sketch
+function setupComposerLifecycle( + i18nInternal: I18nInternal, + composer: Composer +): void { + if (i18nInternal.__composerExtend) { + ;(composer as any)[DisposeSymbol] = i18nInternal.__composerExtend(composer) + } + let emitter: VueDevToolsEmitter | null = null + if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) { + emitter = createEmitter<VueDevToolsEmitterEvents>() + const _composer = composer as any + _composer[EnableEmitter]?.(emitter) + emitter.on('*', addTimelineEvent) + } + const currentScope = getCurrentScope() + if (currentScope) { + onScopeDispose(() => { + if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) { + emitter?.off('*', addTimelineEvent) + const _composer = composer as any + _composer[DisableEmitter]?.() + } + const dispose = (composer as any)[DisposeSymbol] + if (dispose) { + dispose() + delete (composer as any)[DisposeSymbol] + } + }) + } +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vue-i18n-core/src/i18n.ts` around lines 566 - 610, The isolated branch duplicates lifecycle/devtools wiring (composer extension, devtools emitter setup, and onScopeDispose teardown) that also exists in the local branch; extract that shared logic into a helper (e.g., wireComposerLifecycle or setupComposerLifecycle) and call it from both paths. The helper should accept the created composer and I18nInternal (to run __composerExtend and store DisposeSymbol), create and enable the devtools emitter (using createEmitter, EnableEmitter/DisableEmitter, and emitter.on/off with addTimelineEvent) only when (__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__, and register the same onScopeDispose teardown to remove the emitter listener, disable emitter, and call/delete the DisposeSymbol; replace the duplicated blocks in createComposer call sites (where composer is created and where DisposeSymbol, EnableEmitter, DisableEmitter, emitter, __composerExtend, and onScopeDispose are referenced) with a call to this helper.packages/vue-i18n-core/test/i18n.test.ts (1)
463-503: Add a same-key collision regression test for local vs isolated scopes.The current test (lines 463-503) uses different keys across scopes (
hiin local,statusin isolated). Add a test where both scopes use the same key to verify that isolated scope properly isolates from local scope when keys collide—this closes a regression gap.Suggested test case
+ test('isolated scope does not collide with local scope on same key', async () => { + const i18n = createI18n({ + locale: 'en', + messages: { en: { status: 'from global' } } + }) + + let localComposer: Composer + let isolatedComposer: Composer + const App = defineComponent({ + setup() { + localComposer = useI18n({ + messages: { en: { status: 'from local' } } + }) as Composer + isolatedComposer = useI18n({ + useScope: 'isolated', + messages: { en: { status: 'from isolated' } } + }) as Composer + return {} + }, + template: `<p>foo</p>` + }) + await mount(App, i18n) + + expect(localComposer.t('status')).toEqual('from local') + expect(isolatedComposer.t('status')).toEqual('from isolated') + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/vue-i18n-core/test/i18n.test.ts` around lines 463 - 503, Add a regression test that verifies isolated composable scopes truly isolate when keys collide with local/component scope: duplicate the existing "coexists with local scope" test pattern but use the same key name in the component local useI18n and in the isolated composable useI18n (e.g., both use 'status' or 'hi') and assert the component Composer.t('status') (or 'hi') returns the component message while the composableResult.status returns the isolated message; reference the same functions and symbols (createI18n, useI18n with useScope: 'isolated', useMyComposable, localComposer variable, composableResult, Composer, and App) so the new test sits alongside the existing one and ensures collision isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/guide/advanced/composition.md`:
- Around line 471-535: Update the wording in the isolated scope tip to say the
isolated scope inherits locale from the parent/global scope (not just “global
scope”) to match runtime behavior (see composerOptions.__root set to
parentComposer || gl in the core); change the sentence in the EN isolated scope
section (the NOTE tip) to mention “parent/global scope” and also audit and
update the JP and ZH translations of the same tip to the equivalent
parent/global phrasing so all locales are consistent with useI18n and Composer
behavior.
---
Nitpick comments:
In `@packages/vue-i18n-core/src/i18n.ts`:
- Around line 566-610: The isolated branch duplicates lifecycle/devtools wiring
(composer extension, devtools emitter setup, and onScopeDispose teardown) that
also exists in the local branch; extract that shared logic into a helper (e.g.,
wireComposerLifecycle or setupComposerLifecycle) and call it from both paths.
The helper should accept the created composer and I18nInternal (to run
__composerExtend and store DisposeSymbol), create and enable the devtools
emitter (using createEmitter, EnableEmitter/DisableEmitter, and emitter.on/off
with addTimelineEvent) only when (__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) &&
!__NODE_JS__, and register the same onScopeDispose teardown to remove the
emitter listener, disable emitter, and call/delete the DisposeSymbol; replace
the duplicated blocks in createComposer call sites (where composer is created
and where DisposeSymbol, EnableEmitter, DisableEmitter, emitter,
__composerExtend, and onScopeDispose are referenced) with a call to this helper.
In `@packages/vue-i18n-core/test/i18n.test.ts`:
- Around line 463-503: Add a regression test that verifies isolated composable
scopes truly isolate when keys collide with local/component scope: duplicate the
existing "coexists with local scope" test pattern but use the same key name in
the component local useI18n and in the isolated composable useI18n (e.g., both
use 'status' or 'hi') and assert the component Composer.t('status') (or 'hi')
returns the component message while the composableResult.status returns the
isolated message; reference the same functions and symbols (createI18n, useI18n
with useScope: 'isolated', useMyComposable, localComposer variable,
composableResult, Composer, and App) so the new test sits alongside the existing
one and ensures collision isolation.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
docs/guide/advanced/composition.mddocs/jp/guide/advanced/composition.mddocs/zh/guide/advanced/composition.mdpackages/vue-i18n-core/src/components/base.tspackages/vue-i18n-core/src/i18n.tspackages/vue-i18n-core/test/i18n.test.ts
| ## Isolated scope | ||
|
|
||
| The isolated scope creates an independent Composer instance that is **not tied to the component**. This is useful when you want to use `useI18n` inside a composable with its own translation messages, without conflicting with the component's local scope. | ||
|
|
||
| ### Why isolated scope? | ||
|
|
||
| When a component and a composable both call `useI18n` with local scope, the second call conflicts with the first because only one local scope per component is allowed. The isolated scope solves this by creating a Composer that: | ||
|
|
||
| - Is **not registered** with the component's uid (no duplicate detection) | ||
| - Does **not propagate** to child components via `provide` | ||
| - Does **not merge** SFC i18n custom blocks | ||
| - **Inherits locale** from the parent/global scope (by default) | ||
| - **Falls back** to the parent/global scope for missing translation keys | ||
|
|
||
| ### Usage in composables | ||
|
|
||
| <!-- eslint-skip --> | ||
|
|
||
| ```ts | ||
| // useProjectStatus.ts | ||
| import { computed } from 'vue' | ||
| import { useI18n } from 'vue-i18n' | ||
|
|
||
| export function useProjectStatus(project) { | ||
| const { t } = useI18n({ | ||
| useScope: 'isolated', | ||
| messages: { | ||
| en: { active: 'Active', inactive: 'Inactive' }, | ||
| ja: { active: '稼働中', inactive: '停止中' } | ||
| } | ||
| }) | ||
|
|
||
| return computed(() => project.isActive ? t('active') : t('inactive')) | ||
| } | ||
| ``` | ||
|
|
||
| <!-- eslint-skip --> | ||
|
|
||
| ```vue | ||
| <!-- MyComponent.vue --> | ||
| <script setup> | ||
| import { useI18n } from 'vue-i18n' | ||
| import { useProjectStatus } from './useProjectStatus' | ||
|
|
||
| // Local scope for the component | ||
| const { t } = useI18n({ | ||
| messages: { | ||
| en: { title: 'Project Dashboard' }, | ||
| ja: { title: 'プロジェクトダッシュボード' } | ||
| } | ||
| }) | ||
|
|
||
| // Composable with isolated scope — no conflict! | ||
| const status = useProjectStatus(project) | ||
| </script> | ||
|
|
||
| <template> | ||
| <h1>{{ t('title') }}</h1> | ||
| <p>{{ status }}</p> | ||
| </template> | ||
| ``` | ||
|
|
||
| :::tip NOTE | ||
| The isolated scope inherits locale from the global scope by default. When the global locale changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`. | ||
| ::: |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify doc wording vs runtime behavior for isolated scope inheritance.
echo "=== Checking locale inheritance wording in documentation ==="
rg -n "Inherits locale|inherits locale|parent/global scope|global scope by default" \
docs/guide/advanced/composition.md \
docs/jp/guide/advanced/composition.md \
docs/zh/guide/advanced/composition.md 2>/dev/null || echo "Documentation files not found, checking available files..."
echo -e "\n=== Checking for locale inheritance patterns ==="
rg -n "locale.*inherit|inherit.*locale" docs/guide/advanced/composition.md -A2 -B2 2>/dev/null || echo "Pattern not found"
echo -e "\n=== Checking runtime implementation ==="
rg -n "parentComposer.*gl|__root.*parent" packages/vue-i18n-core/src/i18n.ts -B3 -A3 2>/dev/null || echo "Runtime code not found"Repository: intlify/vue-i18n
Length of output: 2113
Fix locale inheritance wording inconsistency in the isolated scope section.
Line 482 correctly describes "inherits locale from the parent/global scope," but line 534 states only "global scope." The runtime implementation (composerOptions.__root = parentComposer || gl, confirmed at lines 574 and 643 in packages/vue-i18n-core/src/i18n.ts) uses the parent Composer first, then falls back to global scope—matching the parent/global behavior. Line 534 should be updated to reflect this:
📝 Suggested wording update (EN)
-The isolated scope inherits locale from the global scope by default. When the global locale changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.
+The isolated scope inherits locale from the parent scope (if available), otherwise from the global scope by default. When the inherited locale source changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.Also update the JP/ZH documentation sections if they contain similar locale inheritance descriptions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/guide/advanced/composition.md` around lines 471 - 535, Update the
wording in the isolated scope tip to say the isolated scope inherits locale from
the parent/global scope (not just “global scope”) to match runtime behavior (see
composerOptions.__root set to parentComposer || gl in the core); change the
sentence in the EN isolated scope section (the NOTE tip) to mention
“parent/global scope” and also audit and update the JP and ZH translations of
the same tip to the equivalent parent/global phrasing so all locales are
consistent with useI18n and Composer behavior.
resolve #2207
related #2098
Summary by CodeRabbit
New Features
Documentation
Tests